C++20 introduced the Concepts library and the corresponding language extensions to template metaprogramming. This post will be a brief introduction to the topic for people already well versed in C++ templates.

What is a concept? Besides being a new keyword in C++20 it is a mechanism to describe constraints or requirements of typename T; it is a way of restricting which types a template class or function can work with. Imagine a simple template function that adds two numbers:

template<typename T> auto add(T a, T b) { return a + b; }

The way it is implemented doesn’t stop us from calling it with std::string as the parameters’ type. With concepts we can now restrict this function template to work only with integral types for example.

But first let’s define two most basic concepts, one which will accept, or evaluate to true, for all types, and another which will reject, or evaluate to false, for all types as well:

template<typename T> concept always_true = true;
template<typename T> concept always_false = false;

Using those concepts we can now define two template functions, one which will accept, or compile with any type as its parameter, and one which will reject, or not compile regardless of the parameter’s type:

template<typename T> requires always_true<T> void good(T) {} // ALWAYS compiles
template<typename T> requires always_false<T> void bad(T) {} // NEVER compiles

Let’s now rewrite the function that adds two numbers using a standard concept std::integral found in the <concepts> header file:

template<typename T> requires std::integral<T> auto add(T a, T b) { return a + b; }

Now this template function will only work with integral types. But that’s not all! There are two other ways C++20 allows us to express the same definition. We can replace typename with the name of the concept and drop the requires keyword:

template<std::integral T> auto add(T a, T b) { return a + b; }

Or go with the C++20 abbreviated function template syntax where auto is used as a function’s parameter type together with the (optional) name of the concept we wish to use:

auto add(std::integral auto a, std::integral auto b) { return a + b; }

I don’t know about you but I really like this short new syntax!

Concepts can be easily combined. Imagine we have two concepts we wish to combine into a third one. Here’s a simple example of how to do it:

template<typename T> concept concept_1 = true;
template<typename T> concept concept_2 = false;
template<typename T> concept concept_3 = concept_1<T> and concept_2<T>;

Alternatively a function or class template can be declared to require multiple concepts (which requires additional parenthesis):

template<typename T> requires(concept_1<T> and concept_2<T>) foo(T) {}

What follows the requires keyword must be an expression that evaluates to either true or false at compile time, so we are not limited to just concepts, for example:

template<typename T> requires(std::integral<T> and sizeof(T) >= 4) void foo(T) {}

The above function has been restricted to working only with integral types that are at least 32 bit.

Let’s look at a more complex example and analyze it line by line:

In line #1 we define a concept called can_add and define an optional variable a of type T. You may be wondering why the requires keyword appears multiple times. It’s because what follows after requires and is within curly braces {} is referred to as a compound requirement. Compound requirements can contain within them other requirements separated by a semicolon ;. If the expression inside is not prefixed by the requires keyword it must only be a valid C++ statement. However, what follows directly after requires without the surrounding curly braces must instead evaluate to true at compile time. Therefore line #3 means that the value of std::integral<T> must evaluate to true. If we remove requires from line #3 it would mean only that std::integral<T> is a valid C++ code without being evaluated further. Similarly line #4 tells us that the sizeof(T) must be greater than or equal to sizeof(int). Without the requires keyword it would only mean whether or not sizeof(T) >= sizeof(int) is a valid C++ expression. Line #5 means several things: a + a must be a valid expression, a + a must not throw any exceptions, and the result of a + a must return type T (or a type implicitly convertible to T). Notice that a + a is surrounded by curly braces that must contain only one expression without the trailing semicolon ;.

We can apply the can_add concept to a template function as follows:

template<typename T> requires can_add<T> T add(T x, T y) noexcept { return x + y; }

Template function add can now only be invoked with types that satisfy the can_add concept.

So far I have limited the examples to standalone template functions, but the C++20 concepts can be applied to template classes, template member functions, and even variables.

Here’s an example of a template struct S with a template member function void func(U); the struct can only be instantiated with integral types and the member function can only be called with floating point types as the parameter:

See the source code below for more examples.


Example program:
concepts.cpp


Leave a Reply