12

Requires-expression

 4 years ago
source link: https://akrzemi1.wordpress.com/2020/01/29/requires-expression/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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 {}

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

template <typename T, bool = requires{}>
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 noextept(*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 inthe 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: the concept itself has 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. 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 inthis 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.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK