Last Update:
C++20 Concepts - a Quick Introduction
Table of Contents
Concepts are a revolutionary approach for writing templates! They allow you to put constraints on template parameters that improve the readability of code, speed up compilation time, and give better error messages.
Read on and learn how to use them in your code!
What is a concept?
In short, a concept is a set of constraints on template parameters evaluated at compile time. You can use them for class templates and function templates to control function overloads and partial specialization.
C++20 gives us language support (new keywords - requires
, concept
) and a set of predefined concepts from the Standard Library.
In other words, you can restrict template parameters with a “natural” and easy syntax. Before C++20, there were various ways to add such constraints. See my other post Simplify Code with if constexpr and Concepts in C++17/C++20 - C++ Stories.
Here’s an example of a simple concept:
template <class T>
concept integral = std::is_integral_v<T>;
The above code defines the integral
concept. As you can see, it looks similar to other template<>
constructs.
This one uses a condition that we can compute through a well-known type trait (from C++11/C++14) - std::is_integral_v
. It yields true
or false
depending on the input template parameter.
We can also define another one using a requires
expression:
template <typename T>
concept ILabel = requires(T v)
{
{v.buildHtml()} -> std::convertible_to<std::string>;
};
This one looks a bit more serious! But after some time, it seems “readable”:
We defined a concept that requires that an object of type T has a member function called buildHtml()
, which returns something convertible to std::string
.
Those two examples should give you a taste; let’s try to use them in some real code.
How to use concepts
In one of the most common case, for a small function template, you’ll see the following syntax:
template <typename T>
requires CONDITION
void DoSomething(T param) { }
You can also use requires clause
as the last part of a function declaration:
template <typename T>
void DoSomething(T param) requires CONDITION
{
}
The key part is the requires
clause. It allows us to specify various requirements on the input template parameters.
Let’s look at a simple function template that computes an average of an input container.
#include <numeric>
#include <vector>
#include <iostream>
#include <concepts>
template <typename T>
requires std::integral<T> || std::floating_point<T>
constexpr double Average(std::vector<T> const &vec) {
const double sum = std::accumulate(vec.begin(), vec.end(), 0.0);
return sum / vec.size();
}
int main() {
std::vector ints { 1, 2, 3, 4, 5};
std::cout << Average(ints) << '\n';
}
Play with code @Compiler Explorer
With the above source code, I used two concepts available in the standard library (std::integral
and std::floating_point
) and combined them together.
One advantage: better compiler errors
If you play with the previous example and write:
std::vector strings {"abc", "xyz"};
auto test = Average(strings);
You might get:
<source>:23:24: error: no matching function for call to 'Average(std::vector<const char*, std::allocator<const char*> >&)'
23 | auto test = Average(strings);
| ~~~~~~~^~~~~~~~~
<source>:10:18: note: candidate: 'template<class T> requires (integral<T>) || (floating_point<T>) constexpr double Average(const std::vector<T>&)'
10 | constexpr double Average(std::vector<T> const &vec) {
| ^~~~~~~
It’s pretty nice!
You can see that the template instantiation failed because your template parameter - const char*
is not an integer or floating-point.
Usually, with templates, before the concepts feature, you could get some long cryptic messages about some failed operation that is not possible on a given type in some deep level of the call stack.
Predefined Concepts
Here’s the list of predefined concepts that we get in C++20 with <concepts>
header:
Core language concepts | Notes |
---|---|
same_as |
|
derived_from |
|
convertible_to |
|
common_reference_with |
|
common_with |
|
integral |
|
signed_integral |
|
unsigned_integral |
|
floating_point |
|
assignable_from |
|
swappable /swappable_with |
|
destructible |
|
constructible_from |
|
default_initializable |
|
move_constructible |
|
copy_constructible |
Comparison concepts | Notes |
---|---|
boolean-testable |
a type can be used in boolean test cases |
equality_comparable /equality_comparable_with |
|
totally_ordered /totally_ordered_with |
Defined in <compare> |
three_way_comparable /three_way_comparable_with |
Object concepts | Notes |
---|---|
movable |
|
copyable |
|
semiregular |
a type can be copied, moved, swapped, and default constructed |
regular |
a type is both semiregular and equality_comparable |
Callable concepts | Notes |
---|---|
invocable /regular_invocable |
|
predicate |
|
relation |
specifies a binary relation |
equivalence_relation |
|
strict_weak_order |
You can find the list here: Concepts library (C++20) - cppreference.com
And here’s my separate blog post on the Callable concepts:
Code Simplification
As you could see, the syntax for concepts and constraints is relatively easy, but still, in C++20, we got much more!
There are various shortcuts and terse syntax that allow us to make template code super simple.
We have several things:
- Abbreviated Function Templates
- Constrained auto
- Terse syntax for concepts
For example:
template <typename T>
void print(const std::vector<T>& vec) {
for (size_t i = 0; auto& elem : vec)
std::cout << elem << (++i == vec.size() ? "\n" : ", ");
}
We can “compress” it into:
void print2(const std::vector<auto>& vec) {
for (size_t i = 0; auto& elem : vec)
std::cout << elem << (++i == vec.size() ? "\n" : ", ");
}
In the above case, I used unconstrained auto
. In general, you can write:
auto func(auto param) { }
And it expands into:
template <typename T>
auto func(T param) { }
It looks similar to what we get with C++14 and generic lambdas (Lambda Week: Going Generic).
Additionally, we can also use constrained auto
:
void print3(const std::ranges::range auto& container) {
for (size_t i = 0; auto && elem : container)
std::cout << elem << (++i == container.size() ? "\n" : ", ");
};
With print3
, I removed the need to pass a vector and restricted it for all ranges.
Play with the code @Compiler Explorer
Here we have:
auto func(concept auto param) { }
Translates into:
template <typename T>
requires concept<T>
auto func(T param) { }
What’s more, rather than specifying template <typename T> requires...
you can write:
template <std::integral T>
auto sum(const std::vector<T>& vec) {
// return ...;
}
The requires
expression
One of the most powerful items with concepts is the requires
keyword. It has two forms:
- the
requires
clause - likerequires std::integral<T>
or similar - the
requires
expression.
The last one is very flexible and allows to specify quite advanced constraints. In the introduction you’ve seen one case with a detection of buildHtml()
member function. Here’s another example:
template<typename T>
concept has_string_data_member = requires(T v) {
{ v.name_ } -> std::convertible_to<std::string>;
};
struct Person {
int age_ { 0 };
std::string name_;
};
struct Box {
double weight_ { 0.0 };
double volume_ { 0.0 };
};
int main() {
static_assert(has_string_data_member<Person>);
static_assert(!has_string_data_member<Box>);
}
Play with code @Compiler Explorer
As you can see above, we can write requires(T v)
, and from now on, we can pretend we have a value of the type T
, and then we can list what operations we can use.
Another example:
template <typename T>
concept Clock = requires(T c) {
c.start();
c.stop();
c.getTime();
};
The above concept restricts an “interface” for basic clocks. We require that it has the three member functions, but we don’t specify what type do they return.
From one perspective, we can say that the requires
expression takes a type and tries to instantiate the specified requirements. If it fails, then a given class doesn’t comply with this concept. It’s like SFINAE but in a friendly and easy-to-express syntax.
I just showed some basic examples to give you a taste, but look at this article from A. Krzemienski: Requires-expression | Andrzej’s C++ blog which expends this topic in more depth.
The updated Detection Idiom
Thanks to Concepts we can now easily detect a function, a member function or even a particular overload. This is much simpler that with complicated SFINAE techniques that we had before.
See my other article on that topic: How To Detect Function Overloads in C++17/20, std::from_chars Example - C++ Stories
Compiler Support
As of May 2021 you can use concepts with all major compilers: GCC (since 10.0), Clang (10.0) and MSVC (2019 16.3 basic support, 16.8 constrained auto, 16.9 abbreviated function templates see notes). Just remember to use appropriate flag for the C++20 standard - -std=c++20
/-std=c++2a
for Clang/GCC, or /std:c++latest
for MSVC.
Summary
It’s just a tip of an iceberg!
Thanks to the introduction of two new language keywords: requires
and concept
, you can specify a named requirement on a template argument. This makes code much more readable and less “hacky” (as with previous SFINAE based techniques…).
Additionally, the Standard Library is equipped with a set of predefined concepts (mainly obtained from existing type traits), making it easier to start.
What’s more, C++20 offers even more language features to make the syntax even more compact. It’s mostly due to constrained auto. In some cases, you won’t even need to write template <>
at the front of your function template!
What I like about this feature is that you can introduce it slowly in your code. You can add concepts here and there, experiment, see how it works. And then gradually use more advanced constructs and apply in other places.
Back to you
Have you tried concepts? What are your first thoughts on that feature?
What are the most important use cases for you?
Share your comments below the article.
References
I've prepared a valuable bonus for you!
Learn all major features of recent C++ Standards on my Reference Cards!
Check it out here: