10

Requires-clause

 4 years ago
source link: https://akrzemi1.wordpress.com/2020/03/26/requires-clause/
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.
neoserver,ios ssh client

In this post we will talk about another C++20 feature related to constraining templates: requires -clause. Although C++20 is due to be published this year, it is not there yet; so we are talking about the future. However, this can already be tested in trunk versions of GCC and Clang online in Compiler Explorer . A requires -clause looks like this:

template <typename T>
  requires is_standard_layout_v<T> && is_trivial_v<T>
void fun(T v); 

int main()
{
  std::string s;

  fun(1);  // ok
  fun(s);  // compiler error
}

It is an additional “clause” in a template declaration that expresses under what condition the constrained template is supposed to work. It looks like this is an ordinary Boolean expression that gives a yes-no answer, but it is not quite so. If we try to negate one of the operands in the declaration:

template <typename T>
  requires is_standard_layout_v<T> && !is_trivial_v<T>
void fun(T v);

The declaration is no longer valid, and the program fails to compile. So, there is more to the story. I assume that you are already familiar with C++20 concepts, at least superficially. In this post we will explore requires -clause in more detail.

The goal of the requires -clause is to determine if the declaration it constrains is visible in certain contexts. For function templates this context is when performing overload resolutions. For class templates it is when selecting class template specialization. For non-template member functions inside class templates it is when performing explicit template instantiation. For now, we will focus on the first context: overload resolution. Consider the following:

template <typename T>
  requires is_trivial_v<T>
void fun(T v) { std::cout << "1"; } 

template <typename T>
void fun(T v) { std::cout << "2"; } 

int main()
{
  std::string s;
  fun(s);  // displays: "2"
}

The first overload is constrained: the T needs to be trivial (copying it is equivalent to memcpy ), the other is not constrained. When determining the viable candidates from overload set fun for type std::string the first overload’s constraint is not satisfied, so it is not visible. There is only one visible overload then, so the choice is unambiguous. The point here is: violating constraints is not a hard error in itself. It could indirectly cause one later; for instance, if no other overload can be found. But constraint violation itself is not a compilation failure.

Two-step constraint satisfaction

Now, let’s look at a more complicated example:

template <typename T>
  requires is_trivial_v<typename T::value_type> 
void fun(T v);

We expect that the nested type T::value_type is trivial. But what if we cannot even give a true/false answer because the type T doesn’t have a nested type? This is the first special property of requires -clause. The Boolean predicate you provide expresses actually two constraints. First: that this expression is well formed. If it is not, the constraint is considered violated and the template is “disabled”. But it is not a program error:

template <typename T>
  requires is_trivial_v<typename T::value_type> 
void fun(T v) { std::cout << "1"; } 

template <typename T>
void fun(T v) { std::cout << "2"; } 

int main()
{
  fun(1);  // displays: "2"
}

As indicated inanother post, not every ill-formedness can be detected this way. If the compiler needs to instantiate a template during this process and this instantiation causes an error (e.g., due to static_assert ) we will get a hard error.

Next, when the predicate is determined to be well formed, the compiler checks if it is an expression that can be evaluated at compile-time and if its type is exactly bool . If not, we get a hard error. This may happen if we use the pre-C++11 type traits. In these ancient times, when we didn’t have constexpr , the easiest way to write a type trait was to use an enum:

template <typename T>
struct is_small
{
  enum { value = sizeof(T) <= 4 };
};

template <typename T>
  requires is_small<T>::value
void fun(T v) { std::cout << "1"; } 

template <typename T>
void fun(T v) { std::cout << "2"; } 

int main()
{
  fun(1);  // compiler error
}

(Although, this example currently works in GCC trunk, against what C++20 will specify.)

Only after the above steps are performed do we try to evaluate the predicate, and use the Boolean result to determine the constraint satisfaction.

Valid predicates

The following constraint is incorrect, and causes a hard compiler error:

template <typename T>
  requires !is_trivial_v<T>
void fun(T v);

Not every expression — even if of type bool — can be put in requires -clause. The reason for this is that if any expression were to be allowed, we would get into parsing ambiguities. Suppose I need to know if a certain conversion operator is present in T . I could write:

template <typename T> 
  requires (bool)&T::operator short
unsigned int foo();

It will always evaluate to true, but I may only be interested in checking the expression validity. No-one would probably declare a template requirement like this, but the point is: if arbitrary expressions are allowed in requires -clause, compiler needs to be prepared to handle them. The target type of this conversion operator appears to be short . But this is only because I hinted this with the indentation. What if I indented this code differently:

template <typename T> 
  requires (bool)&T::operator short unsigned
int foo();

In order to avoid these ambiguities any arbitrary expression that is not sufficiently simple needs to be wrapped in parentheses. By “sufficiently simple” we mean:

  • literals true and false ;
  • names of variables of type bool of forms:
    value
    value<T>
    T::value
    ns::trait<T>::value
    
  • concept-ids, such as Concept<T> ;
  • requires -expressions .

This list covers 80% use cases. Any other expression needs to be wrapped in parentheses. Thus, the following declaration will work:

template <typename T>
  requires (!is_trivial_v<T>)
void fun(T v);

Forcing braces here is an important safety feature: it reminds us that !predicate is not the opposite of predicate inside a requires -expressions. Recall:

template <typename T>
  requires is_trivial_v<typename T::value_type> 
void fun(T v);

This checks if (1) calling the type trait is well formed and if the call returns true . The opposite of this would be “either calling the type trait is invalid or the call returns false ”. However, the following, if it compiled:

// not valid in C++20
template <typename T>
  requires !is_trivial_v<typename T::value_type> 
void fun(T v);

would mean, “calling the type trait is valid and the call returns false ”. Thus, the required parentheses help make it visually clear that we cannot just negate the predicate in order to obtain the opposite meaning.

As a side note, expressing “either calling the type trait is invalid or the call returns false ” is possible but a bit more complicated:

template <typename T>
concept value_type_valid_and_trivial 
  = is_trivial_v<typename T::value_type>; 

template <typename T>
  requires (!value_type_valid_and_trivial<T>)
void fun(T v);

Conjunction and Disjunction

There is one more thing that is allowed as a predicate in requires -clause: constraint conjunctions and disjunctions. They look like logical operators && and || but they behave differently.

template <typename T, typename U>
  requires std::is_trivial_v<typename T::value_type>
        || std::is_trivial_v<typename U::value_type>
void fun(T v);

The above declaration means that there are two sub-constraints, and that it is sufficient if only one of them is satisfied in order for the full constraint to be satisfied. To appreciate this, consider the following use:

std::optional<int> oi {};
int i {};
fun(i, oi);

Type int::value_type is invalid; therefore, the first sub-constraint is not satisfied. But disjunction operator separates two sub-constraints, so the ill-formedness of the first does not prevent the second from being satisfied. And indeed, optional<int>::value_type is a valid and trivial type. Thus in the end, the full constraint is satisfied and the above function call works. Additionally, as indicated inthis post, the satisfaction of sub-constraints is determined lazily left-to-right. In case of disjunction, if we find one sub-constraint that is satisfied, we do not even attempt to validate subsequent ones. This can sometimes have an important effect on the program correctness.

Now consider a slightly different — at least visually — constraint:

template <typename T, typename U>
  requires (std::is_trivial_v<typename T::value_type>
         || std::is_trivial_v<typename U::value_type>)
void fun(T v);

The only thing that is different is the parentheses. But because of this, we no longer have a disjunction of constraints. Now, it is a single constraint with a single expression of type bool , with an ordinary logical-or operator. This time, in order to determine if the above call to fun(i, oi) is valid, we have to first determine the validity of the full expression:

(std::is_trivial_v<int::value_type>
|| std::is_trivial_v<std::optional<int>::value_type>)

This expression is not valid, so the constraint is not satisfied, and the above call to fun is invalid.

Thus, token || represents a disjunction (and similarly, token && , a conjunction) only at the top-level of the requires -clause.

One other observation about constraint conjunctions and disjunctions is that they cannot be used to constraint template parameter packs. If we wanted to change our function fun template to a variadic form:

template <typename Ts>
  requires is_trivial_v<typename Ts::value_type> || ...
void fun(Ts... v);

This will not compile, because ellipsis is not allowed for constraint disjunction or conjunction. We can wrap this into parentheses, thus forming a fold-expression:

template <typename Ts>
  requires (is_trivial_v<typename Ts::value_type> || ...)
void fun(Ts... v);

But now, again, it is no longer a constraint disjunction; instead, we get a single constraint with a logical-or operator. Forming a “variadic constraint disjunction” is possible, but requires the introduction of a concept:

template <typename T>
concept trivial_value_type 
  = std::is_trivial_v<typename T::value_type>;

template <typename Ts>
  requires (trivial_value_type<Ts> || ...)
void fun(Ts... v);

Now, the constraint in requires -clause is still a single expression, but the special powers of concepts encapsulate the two-stage of constraint validation (well-formedness and returning true ) for individual types from the parameter pack, so that the failed check for one type does not affect the check for the second type.

So, we can see again that concepts have special powers when it comes to validating constraints. But this is a subject for another post.

Overload ordering by constraints

Constrains play a special role in ordering viable candidate functions in the overload set. Consider:

template <typename T>
void fun(T v);

template <typename T>
  requires std::is_integral_v<T>
void fun(T v);

int main()
{
  fun("X"); // calls first overload
  fun(1);   // calls second overload
}

The first function call, fun("X") , unsurprisingly chooses the first overload: the second overload is not viable because its constraint is not satisfied for type const char* . However, for the second function call both overloads are viable. So, there potentially could be an ambiguity, but the constrained templates have special properties. One here is that a constrained function template (provided that the constraint is satisfied) is a better match than an unconstrained function template. It can be literally any constraint, including a trivial one:

template <typename T>
void fun(T v);

template <typename T>
  requires true
void fun(T v);

int main()
{
  fun("X"); // calls second overload
  fun(1);   // calls second overload
}

Now, for every single type T , both functions will be viable candidates, but the second one will always win by the virtue of only having a constraint.

What if two overloads are constrained? This, of course depends on what the constraints are, but the behavior may still be surprising:

template <typename T>
  requires std::is_standard_layout_v<T>
void fun(T v);

template <typename T>
  requires std::is_standard_layout_v<T>
        && std::is_trivial_v<T>
void fun(T v);

The first overload accepts types that are standard-layout (their members, if any, are laid out as structs in C). The second one expresses a conjunction of constraints: the type must be standard-layout and it must also be trivial. Let’s check them for a type like this one:

struct P
{
  int x;
  int y;
  P(P const& p) : x(p.x), y(p.y) { std::cout << "copy"; } 
};

It is standard-layout, but not trivial (you cannot replace the copy constructor with memcpy ). In this case only the first overload is viable (its constraints are satisfied), so it gets chosen.

Now, if we call this function for an object of type int , which is both standard-layout and trivial:

fun(1); // compiler error: ambiguous call

We get a compiler error: the call is ambiguous, as both candidates are equally good. Even though the second only adds constraints atop of the first one’s constraints. You might find it surprising if you already learned about C++ concepts and their capabilities. But our overloads do not use concepts: they use type traits instead. So, let’s add a concept into the picture:

template <typename T>
concept standard_layout 
  = std::is_standard_layout_v<T>;

template <typename T>
  requires standard_layout<T>
void fun(T v);

template <typename T>
  requires standard_layout<T> && std::is_trivial_v<T>
void fun(T v);

int main()
{
  fun(1); // selects second overload
}

Now it works! The second overload is selected because it is more constrained than the first. But wasn’t the same the case in the previous example? In some sense yes: both you and I know that in the example with type traits the second overload was more refined than the first one. But the compiler doesn’t look at it this way. In the most general case it would be impossible to tell which of any two constraints is more constraining. Therefore, the compiler will not attempt to figure it out, even in the simple cases. Especially, because it would be difficult to tell which cases are “sufficiently simple”. The compiler will only start playing this game if you use concepts. So we can see a yet another special power of concepts. But this is a subject for another post.

In the closing of this one, let me only mention three things. We have seen that a requires -clause can introduce constraints for templates; but this is not the only way for declaring constraints. We will see another one in another post. Second, in this post we have seen how function templates are constrained. But it is also possible to constrain class templates and class template specializations:

// primary class template
template <typename T>
class optional
{
  bool _initialized = false;
  union {T val; char naught;} _storage;
};

// class template specialization
template <typename T>
  requires std::is_trivial_v<T>
class optional<T>
{
  bool _initialized = false;
  T _storage;
};

optopnal<int> oi; // selects the specialization

It is also possible to constrain non-template member functions of class templates:

template <typename T>
struct wrapper
{
  T _value;
  void operator() ()
    requires std::is_invocable_v<T>
  { _value(); }

  void reset(T v) { _value = v; }
};

// explicit instantiation:
template struct wrapper<int>;

Because member function operator() has a requires-clause, it is not instantiated when a full class template instantiation is requested but the constraint is not satisfied. This is very useful when testing class template implementations, as described inthis post. Unfortunately, at the time of writing this post, neither Clang nor GCC implement this functionality in their trunk versions. The only Concepts implementaiton that has it, that I am aware of, is the previous implementation of Concepts in Clang by Saar Raz. You can compare the implementations here .

Finally, you can use requires -expressions inside a requires -clause, which gives a funny effect:

template <typename T>
  requires requires(T x, T y) { bool(x < y); }
bool equivalent(T const& x, T const& y)
{
  return !(x < y) && !(y < x);
};

This causes a similar emotional response as noexcept(noexcept(expr)) , but is occasionally useful.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK