A concept is a compile-time predicate that's used in conjunction with templates. The C++20 standard definitely boosted generic programming by providing more compile-time opportunity for the developer to communicate its intention. We can visualize concepts such as requirements (or constraints) the user of the template must adhere to. Why do we need concepts? Do you have do define concepts by yourself? This recipe will answer these and many more questions.
Understanding concepts
How to do it...
In this section, we will develop a concrete template example using concepts:
- We want to create our own version of the std::sort template function from the C++ standard library. Let's start by writing the following code in a .cpp file:
#include <algorithm>
#include <concepts>
namespace sp
{
template<typename T>
requires Sortable<T>
void sort(T& container)
{
std::sort (begin(container), end(container));
};
}
- Now, let's use our new template class with the constraint that the type we pass, an std::vector, must be sortable; otherwise, the compiler will notify us:
int main()
{
std::vector<int> myVec {2,1,4,3};
sp::sort(vec);
return 0;
}
We'll look at the details in the next section.
How it works...
I strongly believe concepts were the missing feature. Before them, a template didn't have a well-defined set of requirements, nor, in the case of a compilation error, a simple and brief description of it. These are the two pillars that drove the design of the concepts feature.
Step 1 includes the algorithms include for the std::sort method and the concepts header. To not confuse the compiler and ourselves, we encapsulated our new template in a namespace, sp. As you can see, there is a very minimal difference compared to the classical templates we used to use and the difference is with the requires keyword.
requires communicates to the compiler (and to the template user) that this template is only valid with a T Sortable type (Sortable<T>). OK; what is Sortable? This is a predicate that is only satisfied if it is evaluated to true. There are other ways to specify a constraint, as follows:
- With the trailing requires:
template<typename T>
void sort(T& container) requires Sortable<T>;
- As a template parameter:
template<Sortable T>
void sort(T& container)
I personally prefer the style in the How to do it... section as it is more idiomatic and, more importantly, allows us to keep all the requires together, like so:
template<typename T>
requires Sortable<T> && Integral<T>
void sort(T& container)
{
std::sort (begin(container), end(container));
};
In this example, we want to communicate that our sp::sort method is valid with type T, which is Sortable and Integral, for whatever reason.
Step 2 simply uses our new customized version of sort. To do this, we instantiated a vector (which is Sortable!) and passed in input to the sp::sort method.
There's more...
There might be cases where you need to create your own concept. The standard library contains plenty of them, so it is a remote probability that you'd need one. As we learned in the previous section, a concept is a predicate if and only if it is evaluated as true. The definition of a concept as a composite of two existing ones might look like this:
template <typename T> concept bool SignedSwappable()
{ return SignedIntegral<T>() && Swappable<T>(); }
Here, we can use the sort method:
template<typename T>
requires SignedSwappable<T>
void sort(T& container)
{
std::sort (begin(container), end(container));
};
Why is this cool? For a couple of reasons:
- It lets us immediately know what the template expects without getting lost in implementation details (that is, the requirements or constraints are explicit).
- At compile time, the compiler will evaluate whether the constraints have been met.
See also
- A Tour of C++, Second Edition, B. Stroustrup: Chapter 7.2 and Chapter 12.7 for a complete list of concepts defined in the standard library.
- https://gcc.gnu.org/projects/cxx-status.html for a list of C++20 features mapped with GCC versions and status.