Requires-clause

Update. This post in its original form contained incorrect information about the meaning of parentheses inside requires-clauses in section Conjunction and Disjunction. The section has now been changed to correct this. The updated text is in blueish color. Even if you have already read this post, I encourage you to read the section again. I am sorry for having misinformed the readers in the original text. I also want to thank James Pfeffer for pointing out this error.

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 in another 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, U u); 

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 in this 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.

Just to dispel an incorrect claim that I made in the original version of this post, let me mention that the above example will also work if I we put parentheses around the expression containing the disjunction:

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, U u); 

This is still a disjunction of two atomic constraints. As a consequence of this behavior,
it is not easy to use ordinary logical operators || and && inside a requires-clause.
If, for some reason, we wanted to have the behavior of the good old logical-or operator, “first check if the entire expression compiles and then determine its Boolean value”, we would have to do some additional tricks:

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

While the top-level token ||, or the one nested in parentheses, represents a disjunction, any occurrence of token || inside an arbitrary expression that is not && or || or parentheses is treated as an ordinary logical-or operator.

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 it is no longer a constraint disjunction; instead, we get a single atomic constraint with semantics “first check if all of this compiles and only then determine Boolean value”. 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.

What if we used a really degenerate constraint?

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

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

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

Both Clang and GCC select the second overload in both calls, based on the mechanical rule that any constrained template is better than the unconstrained template. However, according to C++20 Standard (not yet official) this program is ill-formed, no diagnostic required. This is because we have two function templates that are functionally equivalent for all Ts. Compilers are not required to diagnose this, and they are allowed to assume that such situation will never occur. So, while it might happen to work today on your compiler, another compiler in the future could choose to reject this code. Another compiler can select the first overload.

Now, what if two overloads are constrained, but with different constraints? 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;
};

optional<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 in this 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.

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

6 Responses to Requires-clause

  1. Endzior says:

    You are missing the triple dots from the fold expression functions templated names, other than that a great read.

    Thank you Andrzej!

  2. rdonch says:

    Thanks for the overview.

    I found a couple of mistakes: in the “Conjunction and Disjunction” section, the function is probably supposed to be “void fun(T v1, U v2)” rather than “void fun(T v)”. Also, “optional” is misspelled as “optopnal” at one point.

  3. James Pfeffer says:

    Thanks for a really useful article.

    I tried the the second version of fun from the “Conjunction and Disjunction” section with the following complete program.

    #include <type_traits>
    #include <optional>
    
    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, U u)
    {
        return;
    } 
    
    int main()
    {
        std::optional<int> oi {};
        int i {};
        fun(i, oi);
    
    
        return 0;
    }
    

    The link to this code on compiler explorer is https://godbolt.org/z/HzK8GT. Despite the presence of parentheses around the requires clause, this program compiles and runs with both clang and gcc. Is this an error?

    • Hi James. Clang and GCC are right about this. The only error is in my description. I am sorry about this. I have just corrected the post to reflect this behavior. Thank you for pointing this bug out.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

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