Concepts in C++20

Concepts allow us to add requirements to template arguments.

There are different types of constraints, e.g.

  • Syntactic requirement – e.g. what operations need to be defined.
  • Semantic requirements – i.e. what behaviour is required of the operations.

For example, an iterator needs to have begin() and end() operations and also needs to implement the increment operator. These are syntactic requirements.

An example of a semantic requirement could commutativity under addition (i.e. a+b = b+a).

Note that only syntactic requirements can be checked statically by the compiler. Semantic requirements cannot be checked automatically.

Predefined concepts

The concepts library has many predefined concepts. Here are a few examples:

  • std::integral
  • std::signed_integral
  • std::unsigned_integral

The names are fairly self-explanatory, but more details can be found here:

Using concepts

To use an existing concept (as a requirement on a template parameter), we make use of the requires keyword (C++20). For example:

#include <concepts>

template <typename T> 
requires std::integral<T>
void myfunc(T x)
{
   // Implementation here...
}

Here, we have defined a template function myfunc which requires the template argument T to be of integral type. If we try to use the function with a non-integral type (such as double) we’ll get a compiler error.

Defining concepts

How do we define our own concepts? Suppose we want a concept called Printable which requires that a type implements the print() operation:

template<typename T>
concept Printable = requires (T a)
{
    a.print();
};

If the template type T implements the print() member function, it satisfies our Printable concept.

To use the concept in a template function definition, we employ the requires keyword:

template <typename T> 
requires Printable<T>
void exampleFn(T x)
{
   // Implementation here...
}

Conjuction and disjunction

We can use existing concepts as building blocks for more high-level concepts.

For example, the predefined std::unsigned_integral concept is defined in C++20 as follows:

template <class T>  
concept unsigned_integral = std::integral<T> && !std::signed_integral<T>;

This is an example of a conjuntion, where the && operator is used to specify that two constraints must be satisfied.

We can also build concepts using the || operator. This is called a disjunction.

Iterators

The C++20 concepts library defines several new concepts relating to iterators. These are very important as they underpin the C++20 ranges library.

These include:

  • std::input_iterator – requires that referenced values can be read.
  • std::output_iterator – requires that referenced values can be written to.
  • std::forward_iterator – requires that we can iterate using the increment operator.
  • std::bidirectional_iterator – requires that we can iterate in both directions (increment and decrement).
  • std::random_access_iterator – requires that we can access elements via the subscript operator T[].

As an example, if we wanted to write our own generic sort function, we could explicitly require a random_access_iterator.

Why do we need concepts?

Concepts are useful for expressing intent. If we know what properties are required by a type in order for our algorithm to work, we can be explicit about this in its definition.

Concepts also tend to result in better error messages. When mistakes are present in templated code, the messages from the compiler can be extremely verbose and baffling. With concepts, requirements are checked at the point of use and you should see a more concise and relevant error message.