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 operatorT[]
.
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.