0

requires expressions and requires clauses in C++20

 1 year ago
source link: https://mariusbancila.ro/blog/2022/06/20/requires-expressions-and-requires-clauses-in-cpp20/
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.

requires expressions and requires clauses in C++20

Posted on June 20, 2022June 20, 2022 by Marius Bancila

The C++20 standard added constraints and concepts to the language. This addition introduced two new keywords into the language, concept and requires. The former is used to declare a concept, while the latter is used to introduce a requires expression or a requires clause. These two could be confusion at first, so let’s take a look at which is which and what is their purpose.

Let’s start with the following example:

requires-700x495.png

In this snippet, we have the following:

  • A concept, called Composable, whose body consists of a requires expression (containing a single constraint). The requires expression is requires(T a, T b) { a + b; }.
  • A function template called add, that constrains its template argument T using the Composable concept within a requires clause, which is requires Composable<T>.
  • A function template also called add, that constrains its template argument T using the requires expression requires(T a, T b) { a + b; } directly in a requires clause (requires requires(T a, T b) { a + b; }).

Let’s discuss them one by one.

requires expressions

A requires expression is a compile-time expression of type bool that describes the constraints on one or more template arguments. There are several categories of requires expressions:

  • simple requirements, such as the one we just saw earlier.
  • type requirements, requires that a named type is valid; such a requirement starts with the typename keyword
  • compound requirements, assert properties of an expression
  • nested requirements, introduced with the requires keyword, can be used to specify additional constraints in terms of local parameters.

Let’s see an example that includes all of these:

template <typename T>
concept Fooable = requires(T a)
// simple requirements
a++; // can be post-incremented
++a; // can be pre-incremented
// type requirements
typename T::value_type; // has inner type member value_type
// compound requirements
{ a + 1 } -> std::convertible_to<T>; // a + 1 is a valid expression AND
// its result must be convertible to T
// nested requirements
requires std::same_as<T*, decltype(&a)>; // operator& returns the same type as T*
template <typename T>
concept Fooable = requires(T a)
{
   // simple requirements
   a++;                                      // can be post-incremented
   ++a;                                      // can be pre-incremented

   // type requirements
   typename T::value_type;                   // has inner type member value_type

   // compound requirements
   { a + 1 } -> std::convertible_to<T>;      // a + 1 is a valid expression AND
                                             // its result must be convertible to T

   // nested requirements
   requires std::same_as<T*, decltype(&a)>;  // operator& returns the same type as T*
};

There are some important things to keep in mind here:

  • A requires expression is a compile-time expression of the type bool and can appear anywhere a compile-time Boolean can appear (such as if constexpr or static_assert statements). Requires expressions are not limited to the body of concepts or in requires clauses.
  • The expressions inside a requires expression are never evaluated. The T a object in the example above does not have a lifetime. It’s never instantiated. The only thing the compiler does is ensuring that the expressions where it is present (such as a++ or a + 1 or decltype(&a)) are valid, i.e. well-formed.
  • Requires expressions in a template are evaluated when the template is instantiated. They can evaluate to either true or false. If the body of a requires expression is empty the expression evaluates to true.

Here is an example of requires expressions used within the body of a function template:

struct point
int x;
int y;
std::ostream& operator<<(std::ostream& os, point const& p)
os << '(' << p.x << ',' << p.y << ')';
return os;
template <typename T>
constexpr bool always_false = std::false_type::value;
template <typename T>
std::string as_string(T a)
constexpr bool has_to_string = requires(T x)
{ std::to_string(x) } -> std::convertible_to<std::string>;
constexpr bool has_stream = requires(T x, std::ostream& os)
{os << x} -> std::same_as<std::ostream&>;
if constexpr (has_to_string)
return std::to_string(a);
else if constexpr (has_stream)
std::stringstream s;
s << a;
return s.str();
static_assert(always_false<T>, "The type cannot be serialized");
int main()
std::cout << as_string(42) << '\n';
std::cout << as_string(point{1, 2}) << '\n';
std::cout << as_string(std::pair<int, int>{1, 2}) << '\n'; // error: The type cannot be serialized
struct point
{
   int x;
   int y;
};

std::ostream& operator<<(std::ostream& os, point const& p)
{
   os << '(' << p.x << ',' << p.y << ')';
   return os;
}

template <typename T>
constexpr bool always_false = std::false_type::value;

template <typename T>
std::string as_string(T a)
{
   constexpr bool has_to_string = requires(T x)
   {
      { std::to_string(x) } -> std::convertible_to<std::string>;
   };

   constexpr bool has_stream = requires(T x, std::ostream& os)
   {
      {os << x} -> std::same_as<std::ostream&>;
   };

   if constexpr (has_to_string)
   {
      return std::to_string(a);
   }
   else if constexpr (has_stream)
   {
      std::stringstream s;
      s << a;
      return s.str();
   }
   else
      static_assert(always_false<T>, "The type cannot be serialized");
}

int main()
{
   std::cout << as_string(42) << '\n';
   std::cout << as_string(point{1, 2}) << '\n';
   std::cout << as_string(std::pair<int, int>{1, 2}) << '\n'; // error: The type cannot be serialized
}

In this example, the as_string function is an uniform interface to serialize objects to string. For this purpose, it uses either the std::to_string function or the overloaded output stream operator <<. To select between these, two requires expressions are used; their purpose is to identify whether the expressions std::to_string(x) or os << x are valid (where x is a T) and what is their return type. As a result, calling as_string(42) and as_string(point{1, 2}) are both successful, but as_string(std::pair<int, int>{1, 2}) triggers a compiling error because neither of the two requires expressions is evaluated to true.

requires clauses

A requires clause is a way to specify a constraint on a template argument or function declaration. The requires keyword must be followed by a constant expression. The idea is though, that this constant expression should be a concept or a conjunction/disjunction of concepts. Alternatively it could also be a requires expression, in which case we have the curious syntax requires requires expr (that we’ve seen in the image above).

Here is an example of a requires clause:

template <typename T>
T increment(T a) requires std::integral<T>
return a + 1;
template <typename T>
T increment(T a) requires std::integral<T>
{
   return a + 1;
}

The same requirement can be expressed as follows, with the requires clause following the template parameter list:

template <typename T> requires std::integral<T>
T increment(T a)
return a + 1;
template <typename T> requires std::integral<T>
T increment(T a)
{
   return a + 1;
}

This example used a single concept in the require clause. It constraints the template argument T to be of an integral type. The next snippet shows a disjunction of two concepts, that extends the constraint to include floating-point types too:

template <typename T>
T increment(T a)
requires std::integral<T> || std::floating_point<T>
return a + 1;
template <typename T>
T increment(T a)
   requires std::integral<T> || std::floating_point<T>
{
   return a + 1;
}

If we want to allow any type T for which the operation a + 1 is supported, we can use a requires clause with a requires expression, as follows:

template <typename T>
T increment(T a)
requires requires (T x) { x + 1; }
return a + 1;
template <typename T>
T increment(T a)
   requires requires (T x) { x + 1; }
{
   return a + 1;
}

This example is perhaps a little bit silly, since we just replicate the expression in the return statement, but its purpose is to demonstrate the syntax for requires clauses.

However, not every expression of the type bool is allowed in a requires clause. Here is an example that does not work:

template <typename T>
T increment(T a) requires !std::floating_point<T>
return a + 1;
template <typename T>
T increment(T a) requires !std::floating_point<T>
{
   return a + 1;
}

Here is what you get with Clang/gcc (the VC++ compiler doesn’t seem to have a problem with this):

prog.cc:5:27: error: parentheses are required around this expression in a requires clause
T increment(T a) requires !std::floating_point<T>
^~~~~~~~~~~~~~~~~~~~~~~
prog.cc:5:27: error: parentheses are required around this expression in a requires clause
T increment(T a) requires !std::floating_point<T>
                          ^~~~~~~~~~~~~~~~~~~~~~~
                          (                      )

The expression in a requires clause may contain the following:

  • the bool literals true and false
  • names of variables of the bool type (such as value, value<T>, T::value)
  • concepts (such as std::integral<T>)
  • requires expressions

For anything else, wrapping parenthesis must be used as follows:

template <typename T>
T increment(T a) requires (!std::floating_point<T>)
return a + 1;
template <typename T>
T increment(T a) requires (!std::floating_point<T>)
{
   return a + 1;
}

Wrapping up

So what is the purpose of requires expressions and requires clauses?

  • A requires expression is Boolean expression that can be used with a requires clause or to define the body of a named concept (which in turn is used with a requires clause). Its purpose is to determine if one or more expressions are well-formed. It has no side effects and does not affect the behavior of the program.
  • A requires clause uses a compile-time Boolean expression to define requirements on template arguments or function declarations. It affects the behavior of a program, determining whether a function participates in overload resolution or not or whether a template instantiation is valid.

There is more to these topics than what I have presented here. To learn more about these, see the following articles:

Like this:

Loading...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK