Requires-expression

This post is about a C++20 feature, so we will be talking about the future. However, this is a very near feature, C++20 is expected to go out this year, and concepts look really stable, so the chances are high that they will become standard pretty soon. Meanwhile, two experimental implementations can be tested online in Compiler Explorer. I assume that you are already familiar, at least superficially, with C++20 concepts. In this post we will explore only one part of it: requires-expressions.

This is how a concept declaration would often look like:

template <typename T>
concept Machine = 
  requires(T m) {  // any `m` that is a Machine
    m.start();     // must have `m.start()` 
    m.stop();      // and `m.stop()`
  };

But this is in fact two separate features that happen to work together. One is a concept, the other is a requires-expression. We can declare a concept without a requires-expression:

template <typename T>
concept POD = 
  std::is_trivial<T>::value &&
  std::is_standard_layout<T>::value;

We can also use a requires-expression for other purposes than declaring a concept:

template <typename T>
void clever_swap(T& a, T& b)
{
  constexpr bool has_member_swap = requires(T a, T b){ 
    a.swap(b); 
  };

  if constexpr (has_member_swap) {
    a.swap(b);
  }
  else {
    using std::swap;
    swap(a, b);
  }
}

In this post we will look at requires-expression as a stand-alone feature, and explore its limits.

A requires-expression, in short, tests if a given set of template parameters provides a desired interface: member functions, free functions, associated types, and more. In order to be able to do this, a new sub-language has been devised for describing what is required of an interface. For instance, in order to check if a given type Iter can be incremented, we can write:

requires(Iter i) { ++i; }

A couple of things to note here. We usually (although this is not strictly necessary) want Iter to be a template parameter, so the above code would appear inside a template. The above piece of code is an expression of type bool, so it can appear wherever a Boolean expression can appear:

template <typename Iter>
struct S
{
  static_assert(requires(Iter i){ ++i; }, "no increment");
};

It is always a constant expression and can be used inside static_assert or if constexpr or even as template parameter (although you may not be able to test this last one in current gcc experimental version (10.0.1 20200121) due to a bug). The expression ++i is never evaluated. It is as if it was inside sizeof() or decltype(). Its meaning is, “++i must be a valid expression when i is an object of type Iter”. Similarly, i is not really an object: its lifetime never starts, it is never initialized. this Iter i only says that we will be using identifier i to show what expressions are valid. The expression is evaluated, to true or false, when the template is instantiated. If at least one of the listed requirements is not satisfied, the expression evaluates to false; otherwise — if all requirements are satisfied — the expression evaluates to true. This means that if requires-expression has no requirements inside its body, it always evaluates to true. Furhter, the parameter list of requires-expression can be omitted if we are not introducing any parameters, so this:

requires {1;}

is a valid requires-expression that always evaluates to true, and is in fact equivalent to just writing true:

template <typename T, bool = requires{1;}>
struct S;

// same as:
template <typename T, bool = true>
struct S;

The above example is silly, but it helps illustrate what requires-expression is. Also, a requires-expression doesn’t have to appear inside any template, however then its meaning is slightly different: when any requirement is not satisfied, we get a compiler error rather than value false. This could be used to test if a concrete class has a full interface implemented:

#include "SuperIter.hpp"

constexpr bool _ = requires(SuperIter i) {
  ++i; // stop compilatopn if not satisfied
};

Back to expressing constraints. We have seen how to check for an increment. How can we check if a given type has a member function f(), or a static member function? We just need to write an expression that invokes it:

requires(T v) {
  v.f();  // member function
  T::g(); // static member function
  h(v);   // free function
}

How do we check if a function takes an int as an argument? We have to introduce a parameter of type int in parameter list, and use it in the expression:

requires(T v, int i) {
  v.f(i);
}

But what is the type of these expressions? So far we have not specified it, which means that the type is not relevant to us (because we are only interested in side effects, such as incrementing an iterator): it can be any type, or even void. What if we need member function f() to return an int? requires-expression has another syntax for expressing this. But before we use it, we have to answer the question: do we need the function to return exactly int, or is it enough when it returns a type convertible to int? The goal of a requires-expression is that we want to test what we will be able to do with the T. If we are checking if the type of the expression is int, this is because in some template we will be calling this expression like this:

template <typename T>
int compute(T v)
{
  int i = v.f(0);
  i = v.f(i);
  return v.f(i);
}

In order for these expressions to work, function f() doesn’t have to return precisely int. If it returns short it will work for us too. If you want the type to be convertible to int, the syntax for it is

requires(T v, int i) {
  { v.f(i) } -> std::convertible_to<int>;
}

If the return type needs to be exactly int, you type:

requires(T v, int i) {
  { v.f(i) } -> std::same_as<int>;
}

This construct specifies that, (1) the expression in braces needs to be valid, and (2) its return type must satisfy the constraint. Both std::same_as and std::convertible_to are Standard Library concepts. The former takes two types as parameters and checks if they are the same type:

static_assert(std::same_as<int, int>);

It is quite similar to type trait std::is_same, except that it is a concept and it allows us to do some tricks. One of those tricks is that we can “fix” the second parameter of the concept by typing std::same_as<int>. This sort of turns the concept into a requirement that checks if the first parameter, call it T, satisfies std::same_as<T, int>. So, going back to our requirement declaration:

requires(T v, int i) {
  { v.f(i) } -> std::same_as<int>;
}

What we see after the arrow is a concept, and because it is a concept, the requirement reads: “std::same_as<decltype(v.f(i)), int> must be satisfied.”

Note that — unlike in the previous versions of Concepts Lite — it is incorrect to just put a desired return type after the arrow:

requires(T v, int i) {
  { v.f(i) } -> int; // compiler error
}

While the above syntax looks natural, it would not be clear whether same_as or convertible_to property was required. The reason is explained in detail in this paper.

Moving on, if we want to additionally check if function f() is declared not to throw exceptions, there is a syntax for it:

requires(T v, int i) {
  { v.f(i) } noexcept -> std::same_as<int>;
}

Note that the above applies to arbitrary expressions, not only function calls. If we want to say that type T has a member data of type convertible to int we write:

requires(T v) {
  { v.mem } -> std::convertible_to<int>;
}

We can also express that our class has a nested type. We need to use keyword typename:


requires(Iter it) {
  typename Iter::value_type;
  { *it++ } -> std::same_as<typename Iter::value_type>;
}

requires-expression allows us also to evaluate arbitrary predicates on our types. Keyword requires has a special meaning inside the body of requires-expression: the predicate that follows must evaluate to true; otherwise the requirement is not satisfied and the entire requires-expression returns false. If we want to say that the size of our iterator Iter cannot be bigger than the size of a raw pointer we can express it as:

requires(Iter it) {
  requires sizeof(it) <= sizeof(void*);
}

This ability to evaluate an arbitrary predicate is very powerful, and a number of the above constraints can be reduced to this one. For instance, the type of an expression can be declared like this:

requires(Iter it) {
  *it++;

  // with a concept
  requires std::convertible_to<decltype(*it++),
                               typename Iter::value_type>;

  // or with a type trait
  requires std::is_convertible_v<decltype(*it++),
                               typename Iter::value_type>;
}

A no-throw requirement can be alternatively expressed with a noexcept-expression as:

requires(Iter it) {
  *it++;
  requires noexcept(*it++);
}

And finally, because a requires-expression is a predicate itself, we can nest it:

requires(Iter it) {
  *it++;
  typename Iter::value_type;
  requires requires(typename Iter::value_type v) {
    *it = v;
    v = *it;
  };
}

Now, if we want to give a name to the set of constraints that we have created, so that it can be reused in different places in our program as a predicate, we have a couple of options. Wrap it in a constexpr function:

template <typename Iter>
constexpr bool is_iterator()
{
  return requires(Iter it) { *it++; };
}

// usage:
static_assert(is_iterator<int*>());

Use it to initialize a variable template:

template <typename Iter>
constexpr bool is_iterator = requires(Iter it) { *it++; };

// usage:
static_assert(is_iterator<int*>);

Use it to define a concept:

template <typename Iter>
concept iterator = requires(Iter it) { *it++; };

// usage:
static_assert(iterator<int*>);

Use it as an old school type trait

template <typename Iter>
using is_iterator = 
  std::bool_constant<requires(Iter it) { *it++; }>;

// usage:
static_assert(is_iterator<int*>::value);

Some technical details

In order of our analysis of the feature to be complete, we have to mention three details. First, requires-expression uses short-circuiting. It checks the constraints in the order in which they appear, and the moment the first non-satisfied constraint is detected, the checking of subsequent ones is abandoned. This is important for correctness reasons, because — as we have seen in the previous post — constraints where an erroneous construct is produced during the instantiation of a template (class template or function template or variable template) produce a hard compiler error rather than a true/false answer. This can be illustrated with the following example:

template <typename T>
constexpr bool value() { return T::value; }

template <typename T>
constexpr bool req = requires { 
  requires value<T>();
};

constexpr bool V = req<int>;

We use a variable template req to represent the value of the requires-expression in which we need to evaluate a constexpr function value(). Later, when we test our requirement for type int one might expect that it would return false, but it does not. To evaluate the function we have to instantiate the function template. This instantiation triggers an ill-formed function, and this is a hard error. Compilation will just stop. However, if we add a “guard” requirement that checks whether T::value is a valid expression:

template <typename T>
constexpr bool req = requires { 
  T::value;
  requires value<T>();
};

constexpr bool V = req<int>;

The program will compile and initialize V to false owing to short circuiting. For a similar reason the following two C++14 declarations are not equivalent:

template <typename T>
auto fun1()
{
  return T::value;
}

template <typename T>
auto fun2() -> decltype(T::value)
{
  return T::value;
}

Practically any usage of fun1<int> must cause a compilation failure, because even in order to determine its signature we have to instantiate the function template. Whereas in fun2 the error is in the signature, before we try to instantiate the function body, and this can be detected by SFINAE tricks and used as information without causing a compilation failure.

Second observation is that there is a potential for certain misunderstanding as to what is being tested. We have constraints on valid expressions and on Boolean predicates:

requires { 
  expression;         // expression is valid
  requires predicate; // predicate is true
};

If we declare:

requires (T v) { 
  sizeof(v) <= 4;
};

This compiles fine, and may look like we are testing if T has a small sizeof, but in fact the only thing we are testing is whether expression sizeof(v) <= 4 is well formed: not its value. This may become even more confusing if we put a nested requires-expression inside:

requires (T v) { 
  requires (typename T::value_type x) { ++x; };
};

It looks like we might be checking if ++x is a valid expression, but we are not: we are checking if requires (typename T::value_type x) { ++x; } is a valid expression; and it is, regardless if ++x is valid or not. Luckily, none of the existing implementations accepts this code mandated in N4849 (see here). There is also a plan to fix it before C++20 ships, so that the expression that is tested for validity cannot start with requires. You will be forced to write:

requires (T v) { 
  // check if ++x is valid
  requires requires (typename T::value_type x) { ++x; };
};

The third potential gotcha is in how ill-formed types are treated in the parameter list of the requires-expression. Consider:

template <typename T>
struct Wrapper
{
  constexpr static bool value = 
    requires (typename T::value_type x) { ++x; };
};

Now, the type that can potentially be ill-formed (T::value_type) is inside the parameter list rather than the body. If class Wrapper is instantiated with int, will the requires-expression return false, or fail to compile?

The answer given by the Standard is that it should fail to compile: only the constraints in the body of the requires-expression have this property that invalid types and expressions are turned into a Boolean value. This can be counterintuitive for two reasons. First, if we put this requires-expression as a definition of the concept:

template <typename T>
concept value_incrementable = 
  requires (typename T::value_type x) { ++x; };

constexpr bool V = value_incrementable<int>;

This will compile fine. However, in this case the reason is different: concepts themselves have special properties that make this work; but these are not the properties of requires-expression. Similarly, if we put our requires-expression inside a requires-clause of a function template:

// constrained template:
template <typename T>
  requires requires (typename T::value_type x) { ++x; }
void fun(T) {}

// unconstrained template
template <typename T>
void fun(T) {}

f(0);

(This double-requires is not a mistake.) This code will again compile fine, but here the special rules of overload resolution make this work: it is not because of the properties of requires-expression.

Second, it may be difficult to believe that the instantiation of Wrapper<int>::value will fail to compile, because GCC actually compiles it: see here. But this is a bug in GCC.

Practical usages

Finally, for something practical. Where is a requires-clause useful other than for defining a concept? We will see two use cases. The first one we have seen already. It is for defining ad-hoc anonymous template constraints: if we use it only once, there is no point in giving it a name:

template <typename T>
  requires requires (T& x, T& y) { x.swap(y); }
void swap(T& x, T& y) 
{
  return x.swap(y);
}

The first requires introduces the requires-clause; it means, “the constraining predicate follows”. The second requires introduces the requires-expression which is the predicate. BTW, notice here that there is no difference between typing

requires (T& x, T& y) { x.swap(y); }

and

requires (T x, T y) { x.swap(y); }

and even

requires (T&& x, T&& y) { x.swap(y); }

In all three cases x and y are lvalues. (Remember, a named rvalue reference is an lvalue.) The reference sign is just an informal convention.

For the second use case, we could try to write a compile-time test for a “negative” feature. A negative feature is when we provide a guarantee that certain construct will fail to compile. We have seen an example of one in this post. Imagine that we have a member function that starts monitoring some Data. It takes the parameter by a reference, and because there is no intention to modify the data, it is a reference to const object:

struct Machine
{
  void monitor(const Data& data);
  // ...
};

The reference will be stored and used long after function monitor() returns, so we must make sure that this reference is never bound to a temporary. There is a number of ways to accomplish that: e.g., provide another deleted overload, or use lvalue_ref. But we also want to test it: we want to make a static_assert that tests if passing an rvalue to monitor() fails to compile. But we want a true/false answer, rather than a hard compiler error. This task is doable in C++11 with some expert template tricks, but C++20
requires-expression could make the task a bit easier. We could try to write:

static_assert( !requires(Machine m, Data d) { 
  m.monitor(std::move(d));
});

Note the bang operator that expresses our “negative” expectation. But this does not work due to another property of requires-expression: when it is declared outside any template, the expressions and types inside are tested immediately and if any of them is invalid we get a hard error. This is similar to SFINAE: Substitution Failure Is Not An Error when doing overload resolution, but it is an error in non-template contexts. So, in order for our test to work, we have to introduce — even if artificially — a template, one way or another. For instance:

template <typename M>
constexpr bool can_monitor_rvalue = 
  requires(M m, Data d) { m.monitor(std::move(d)); };

static_assert(!can_monitor_rvalue<Machine>);

And that’s it. All secrets of requires-expressions revealed.

This entry was posted in programming and tagged , . Bookmark the permalink.

36 Responses to Requires-expression

  1. Oren says:

    Great article! Thank you
    Can’t wait to have these features stable an widely used.

    * Also, there is a very minor typo: noextept instead of noexcept

  2. George Tsoumplekas says:

    Great article..

  3. Roman says:

    I wonder what the motivation was for making an unsatisfied requires expression outside of a template a hard error. Seems like a weird inconsistency.

  4. Tokeloshe says:

    Thanks for the introduction. I have a question on one of the examples, though:

    Inspired by your following code
    requires(T v) {
    { v.mem } -> std::convertible_to;
    }

    I wanted to create a concept that requires a member variable of type double. So I wrote:
    requires(T t) {
    { t.a} -> std::same_as;
    };
    but I get the error “‘t.a’ does not satisfy return-type-requirement” even for a struct that I’d expect satisfies the requirement. If I change the requirement to a getter function:
    requires(T t) {
    { t.get_a() } -> std::same_as;
    };
    it works as expected but it’s not really what I want.

    Everything I’ve tried can be seen on compiler-explore (compiled with gcc trunk) below with the things that make it fail commented out:
    https://godbolt.org/z/kLD_3d

    Thanks for the help!

    • It starts to work if you change the constraint to

      { t.a } -> std::same_as<double&>;
      

      See: https://godbolt.org/z/UgFxmg

      This is because while the type of the expression t.get_a() is double, the type of expression t.a is double&, even though member a itself is declared as simply double.

      This surprising behavior of decltype has been described in this post.

      • Tokeloshe says:

        Great, thanks!

      • nhuulong says:

        Great! I had two issues, one was not using double “requires”, the other was using `same_as` instead of `same_as`, and your post+comment helped me solve both!

        I noticed that clang++-10 considers t.a to be of type double here, while g++ considers it’s double&. So `std::same_as` will give an unexpected result with g++, `std::same_as` will give an unexpected result with clang++… Maybe it comes from difference in decltype behavior as well?

        I think gcc is in the right here, but the issue right now is that my code cannot work with both compilers when using `same_as` on members.

        My current workaround is to use `std::common_with` or `std::common_reference_with` with either `double` or `double&`. However, I’m not sure it has the same meaning. It should probably be enough for my use case though.

  5. Emily says:

    Why:
    using std::swap;
    swap(a, b);
    instead of:
    std::swap(a, b); <- One less line & less file size

  6. Pingback: C++ Annotated: November 2019 – February 2020 | CLion Blog

  7. Pingback: Первое впечатление от концептов — IT-МИР. ПОМОЩЬ В IT-МИРЕ

  8. Pingback: Первое впечатление от концептов — MAILSGUN.RU

  9. Pingback: Первое впечатление от концептов | INFOS.BY

  10. Pingback: Первое впечатление от концептов / Хабр

  11. Jan says:

    `This will compile fine. However, in this case the reason is different: the concept itself has special properties that make this work; but these are not the properties of requires-expression.`

    What are the special properties?

    • The special properties of concepts that I refer to is that any substitution failure on the right-hand sign of token = when trying to get the true/false result from the concept is turned into result false rather than causing a compiler error. Therefore the following piece of code is correct and initializes V to false:

      template <typename T>
      concept value_incrementable = 
        requires (typename T::value_type x) { ++x; };
      
      constexpr bool V = value_incrementable<int>;
      

      Whereas the following code that uses a variable template rather than a concept causes a hard compiler error when trying to initialize V:

      template <typename T>
      constexpr bool value_incrementable = 
        requires (typename T::value_type x) { ++x; };
      
      constexpr bool V = value_incrementable<int>;
      
  12. netcan says:

    > The answer given by the Standard is that it should fail to compile: only the constraints in the body of the requires-expression have this property that invalid types and expressions are turned into a Boolean value.

    In which paragraph of the standard can I find this answer? I didn’t find it…thank you!

    • The way the Standard defines this is that it first says that building an invalid type or expression makes the program ill-formed, and then it explicitly calls out exceptions to this rule in order to enable SFINAE tricks or, now, requires-expression to work.

      So you will not find a single place that says that the following should fail to compile:

      template <typename T>
      struct Wrapper
      {
        constexpr static bool value = 
          requires (typename T::value_type x) { ++x; };
      };
      

      Instead, you will find places that will tell you the situations, where building an invalid type does not make the program ill-formed. The first such place is http://eel.is/c++draft/expr.prim.req#general-5, but it only defines this special property for the body of the requires-expression: not for the parameter-declaration-clause.

      Other similar places include:
      http://eel.is/c++draft/temp.deduct.general#5
      http://eel.is/c++draft/temp.deduct.general#8
      http://eel.is/c++draft/temp.constr.decl#4

      • netcan says:

        thanks for your reply. The standard says:
        > The substitution of template arguments into a **requires-expression** may result in the formation of invalid types or expressions in its requirements or the violation of the semantic constraints of those requirements. In such cases, the **requires-expression** evaluates to *false*; it does not cause the program to be ill-formed.

        but the parameter-declaration-clause is part of requires-expression, it doesn’t seem to be mentioned in the standard, maybe the standard forgot to mention this? or invalid expression in parameter-declaration-clause is undefined behavior, consider the difference between gcc and clang?

        • My reading of the paragraph that you quoted is that it turns invalid types or expressions in the requirements into a true/false answer. These requirements (written with a different font in the specification) refer only to what is inside the braces: not the parameter clause. So there are two places in the requires-expression that can produce invalid types or expressions: one is in its requirements, the other is in requirement-parameter-list, and this paragraph only favors the first group when changing ill-formedness into a true/false result. So the second group is left in the “default” case, which is ill-formedness.

          Given this interpretation, I would say that Clang is correct and GCC is wrong.

          I asked this very question on CWG reflector when preparing this post, and the people who are responsible for the wording provided this very interpretation. Admittedly, it would be easier, if there was a note that would explicitly handle this case.

      • netcan says:

        The coe example you provided illustrates this point well. thanks a lot.

  13. domtop says:

    When specifying function argument constraint I am trying to understand what looks like inconsistencies and messy syntax – for example

    template <typename T, typename V>
        concept SomeConcept = requires(T t, int v) {
            { t.func1(v) } -> std::same_as<bool>;
              t.func2(SomeOtherConcept<V>);
        };
    

    The argument v has to be specified in the argument list of requires, but I don’t need to do that for func2 argument. Return function constraint must use a concept (which makes sense vs original concept proposal) but the function argument doesn’t. I would have expect to write t.func1(std::same_as). If I had more functions with more arguments, the requires statement would look very long and messy.
    Finally I also need to declare the V type on top of the concept even though it only concerns func2. If I wanted to use SomeConcept in another concept, the list of template types would keep accumulating.
    Is there a better way to structure concepts?

  14. I am not sure if I understood your question or your issue entirely, but maybe the following helps.

    First, concepts are not a tool for answering just any question about a given type (or a set of types). It is likely that if you are doing some more advanced template tricks, you will find that concepts cannot help you. In particular, concepts cannot answer a question if a given type has a member function template.

    Concepts were designed with a use case like STL algorithms in mind: when you pass an iterator type and predicate type to std::find_if they are expected, at this time, to be concrete types (no templates). And the algorithm instantiation also becomes a concrete function. The algorithm will be executing expressions, and we need to determine (only with some accuracy) if these expressions will compile. So, going back to your above example with SomeConcept. You may want to ask:

    Will the following expression work:

    template <SomeConcept T>
    void test1(T t, int v) {
      bool b = t.func1(v);
    }
    

    And in this case the concept should actually say:

    template <typename T, typename V>
        concept SomeConcept = requires(T t, int v) {
            { t.func1(v) } -> std::convertible_to<bool>;
            // ...
        };
    

    I do not think you ever need to say same_as<bool>. same_as only seems to make sense for references, where you want to avoid binding a temporary to a reference.

    You might want to say if the following will work:

    template <SomeConcept T>
    void test2(T t, int v) {
      t.func2(v);
    }
    

    Function func2 doesn’t have to take int: it is enough that it takes something convertible from int. Such requirement can be expressed as:

    template <typename T>
        concept SomeConcept = requires(T t, int v) {
            // ...
            t.func2(v);
        };
    

    You could ask if the following will compile:

    template <CoolConcept T>
    void test3(T t, int v) {
      auto f = t.func2(v);
      f(v);
    }
    

    Then I would use the following concept

    template <typename T>
        concept CoolConcept = requires(T t, int v) {
            { t.func1(v) } -> std::regular_invocable<int>;
        };
    

    If I wanted to ask if the following algorithm will compile:

    template <typename T, typename V>
    void test4(T t, V v) {
      t.func2(v);
    }
    

    The concept would be:

    template <typename T, typename V>
        concept SomeConcept = SomeOtherConcept<V> && requires(T t, V v) {
            t.func2(v);
        };
    

    And the constrained function signature would be:

    template <SomeOtherConcept V, SomeConcept<V> T>
    void test4(T t, V v) {
      t.func2(v);
    }
    

    So, the way I would approach the problem would be to start from the use case. And then see what concepts can do for you, rather than just starting with what syntax is available and if it is symmetric. Maybe if you show us your use case we can see if concepts can help and how to express your constraints.

    A note to your concept:

    template <typename T, typename V>
        concept SomeConcept = requires(T t, int v) {
            { t.func1(v) } -> std::same_as<bool>;
              t.func2(SomeOtherConcept<V>);
        };
    

    you do not have to introduce arguments in requires-expression in order to be able to pass things to the function call. Your first constraint is equivalent to:

    template <typename T, typename V>
        concept SomeConcept = requires(T t) {
            { t.func1(1) } -> std::same_as<bool>;
            // ...
        };
    

    In fact, the construct from your second constraint SomeOtherConcept<V> is an expression that returns true or false depending on whether V satisfies the constraints in SomeOtherConcept, so your second requirement is equivalent to:

    template <typename T, typename V>
        concept SomeConcept = requires(T t, bool b) {
            // ...
            t.func2(b);
        };
    
    • domtop says:

      Here is a more concrete use case – let’s say I have a data source, a data destination, an data transformer of some kind and a Manager calling that transformer. For example that “manager” would decorate the data or log throughput.
      I want to use concepts to get more useful warnings when compiling templated version of the high level manager, get auto-completion and be able to verify in advance that specific implementation for the concept with static_assert.

      I see now some of my previous mistakes so I hope this time the concepts are better written:

      template <typename S>
      concept Source = requires(S src, std::span<uint8_t> buffer) {
          { src.read(buffer) } -> std::convertible_to<int>; //returns bytes read
      };
      
      template <typename D>
      concept Destination = requires(D dest, std::span<uint8_t> buffer) {
          src.write(buffer);
      };
      
      template <typename T, typename S, typename D>
      concept Transformer = Source<S> && Destination<D> && requires(T transformer, S src, D dest) {
          transformer.transform(src, dest);
      };
      
      //finally the "manager"
      template <typename M, typename T, typename S, typename D>
      concept Manager = Transformer<T,S,D> && requires(M manager, T transformer, S src, D dest) {
         manager.process(transformer, src, dest);
      };
      

      Now, let’s say the manager is meant to plug into another higher level class either during construction or as an argument to a member method – the list of templates in the concept declaration keeps getting longer and longer. I was wondering if I am missing something about concepts where the “process” method can specify more locally the constraints on its argument in the same way a member function can be templatized without the whole class being too. Perhaps something that looks like below:

      template <typename M>
      concept Manager = requires(M manager) {
         manager.process(transformer, src, dest) requires Source<src> && Destination<dest> && Transformer<transformer, src, dest>;
      };
      
      • Ok, I think I understand your problem now. And unfortunately, it looks like concepts cannot help here.

        The “contract” for concepts is that when you give a concept a concrete type, it should be able to give you a true/false answer:

        
        class MyConcreteManager // no template
        {
          // ...
        };
        
        static_assert(Manager<MyConcreteManager>);
        

        The question you would like to ask is: can I call

        manager.process(transformer, src, dest)
        

        for any types of transformer, src, dest, as long as they model the corresponding concepts.

        That would be impossible to check: you would have to iterate over all combinations of types. This is similar to the issue that prevents having virtual member function templates.
        It is not the question of missing syntax.

  15. domtop says:

    I had another question trying to write a concept that would look like this:

    template<typename T, typename W>
        concept Encoder = requires(T encoder , W w, std::span<uint8_t> buffer) {
          { encoder.init(w) } -> std::same_as<S>;
          encoder.encode(S& s, buffer);
    }
    

    S is some kind of state class to pass around by the caller. I have trouble expressing the constraint that whatever type “init” returns must be also the type passed to “encode”.

    I can do:

    template<typename T, typename W, typename S>
        concept Encoder = requires(T encoder, S& s , W w, std::span<uint8_t> buffer) {
          { encoder.init(w) } -> std::same_as<S>;
          encoder.encode(s, buffer);
    }
    

    but this requires the caller to specify the state type even though it should not have to care. Ideally the caller should be able to do the following:

    auto state = encoder.init(w);
    encoder.encode(state, buffer);
    
    • Then type exactly the expression that caller needs:

      template<typename T, typename W>
      concept Encoder = requires(T encoder, W w, std::span<uint8_t> buffer) 
      {
          encoder.encode(encoder.init(w), buffer);
      };
      

      Alternatively, what STL concepts do is to introduce an additional requirement that the types produced have names:

      template<typename T, typename W>
      concept Encoder = requires {
          typename T::state_type;
      } 
      && requires(T encoder, typename T::state_type& s, W w, std::span<uint8_t> buffer) 
      {
          { encoder.init(w) } -> std::same_as<typename T::state_type>;
          encoder.encode(s, buffer);
      } ;
      

      Which could be shortened to:

      template<typename T, typename W>
      concept Encoder = requires(T encoder, typename T::state_type& s, W w, std::span<uint8_t> buffer) 
      {
          { encoder.init(w) } -> std::same_as<typename T::state_type>;
          encoder.encode(s, buffer);
      } ;
      

      But I suspect (perhaps incorrectly), the latter has fewer chances of producing better error messages.

  16. Pingback: requires expressions and requires clauses in C++20

  17. Is it possible to use concepts to put requirements on the signature of a method (the type of the parameters)?
    You use this example
    requires(T v, int i) {
    v.f(i);
    }
    but that only checks if f can be called by passing an int. What I would like to know specifically is if it is possible to ensure at compiletime that f takes an int& as parameter and create a compiler error if f(int) or f(float) is supplied.

    In my case, f is a static method and I have currently implemented the check by trying to cast f to a function pointer with a specific type
    typedef void (*DoStuffFunc)(int& val);

    requires(T t) {
    static_cast(T::DoStuff);
    };

    And that works perfectly but I was wondering if there is a way to directly check a constraint on the type of the parameter instead of using this workaround.

    • In general, there is no direct way to require that something is not available in the interface. Requires-expressions are designed to check the converse: if something is available.

      For “negative checks” one has to resort to tricks, like you did. But this is going against the intention of generic programming and template constraints.

      • Thx I thought as much. Though I wouldn’t say I was doing a ‘negative check’

        I mean we can create a constraint ‘Does T have a method called X’ or ‘does T have a casting operator to void*’ and those are considered to be fully in line with the intention of template constraints.

        A constraint that says ‘Does T have a method called X AND does X take an int& parameter’ seems like a reasonable constraint to make. for the same reason that ‘Does T have a method called X’ is a reasonable constraint to make.

  18. Pingback: C++ Map And Unordered_map Template Parameter: Check For Common Behavior Using C++20 Concepts - Programming Questions And Solutions Blog

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.