Let's get comfortable with concepts
02 May 2026
Constraints and concepts are a major feature introduced in C++20 and it was long-awaited by the C++ community.
They provide a way to “constrain” templates to make them more expressive, harder to misuse and easier to debug.
Additionally, we can use constraints and concepts instead of SFINAE to enable or disable parts of the API based on the properties of the types used as template parameters.
We are about to get comfortable with concepts, so prepare for some templates and many practical examples!
Overall, in this tutorial we will explain how to constrain the template types that may be used.
In other words, how to introduce compile-time requirements for our class and function templates.
With this in mind, one may ask: “Why do we need to make things stricter?”
We will soon see that it’s all about expressing our intent in a rather simple way,
without resorting to complex template metaprogramming techniques.
Disclaimer for the language lawyers: No attempt to optimize the code has been made, the focus is on trying to
convey concepts in a clear way without distractions. You have been warned. ⚠️
This is a long tutorial, so here is a table of contents:
- The “problem” with unconstrained templates
- Let’s constrain things a bit
- Making our first concept
- Focusing on readability
conceptvsrequires- How do you
requires? requireswithout curly bracesrequireswith curly braces- Recap: With VS without curly braces
- “Interfaces” for our template types
- API for template types?
- “Interfaces” for template types without concepts: SFINAE
- “Interfaces” for template types with concepts
requiresas a “contract”requiresrequiring…- More
requires - Choosing the right candidate (à la SFINAE)
- Specializing member functions
if constexprandrequires- (Avoid) Concepts that are always satisfied
static_assertand conceptsrequires { requires <true|false> }requires requires { statements...; }autofunction arguments- Concepts with multiple types
- 1st argument deduction
- Concepts with generic lambdas (no template parameter list)
- Concepts with generic lambdas (with template parameter list)
- Concepts with variadic templates
- Concepts with variadic templates and
if constexpr - Concepts with variadic templates and
requires - Takeaways
The “problem” with unconstrained templates
Before we delve into the “advanced” template metaprogramming things you can do with concepts, let’s first focus on the basics: Expressiveness and readability. We start by analyzing why we need concepts, by taking a look at the following snippet:
template <typename Camera>
class AutonomousCar {
Camera mCamera;
public:
// ... A lot of code
};
Assuming we need to implement a Camera for AutonomousCar, we must ensure that Camera
has all the necessary member functions and types that AutonomousCar relies on.
If we don’t, the program will not compile.
The “problem” here is that reading AutonomousCar to figure out what Camera’s interface should look like
might be difficult if AutonomousCar is large and complicated.
To give you a better grasp of what I am trying to convey,
imagine if we were not using templates and instead had something like this:
#include "Camera.h"
class AutonomousCar {
Camera mCamera;
public:
// ... A lot of code
};
Were we not using templates and AutonomousCar and Camera were “normal” classes,
it would be clear what Camera should implement by looking at the relevant interface and documentation.
With templates “the interface” is not as clear and the compiler errors are not always helpful or easy to understand.
The problem with expressiveness and readability of template types goes, in my opinion, beyond getting something to compile. It is also about communicating the intent of the code to the reader in a clear and foolproof way:
template <typename T>
T getMedianNumber(std::vector<T> values) {
std::sort(values.begin(), values.end());
return values[values.size() / 2];
}
What types make sense as T above?
int, double and other numeric types, right? This is mostly due to the, perhaps not ideal, name of the function.
What types can actually be used as T?
Any type that can be compared with < and can be copied.
In other words a std::string would compile, however it is most likely not the intended use case.
One way forward is to make getMedianNumber more explicit:
template<typename T>
T getMedianNumber(std::vector<T> values) {
static_assert(std::is_integral_v<T> || std::is_floating_point_v<T>,
"T must be an integral or floating-point");
std::sort(values.begin(), values.end());
return values[values.size() / 2];
}
This is somewhat better in the sense that compilation will fail if we try to use
getMedianNumber with a type that is not intended to be used. That being said, it is somewhat not ideal
as one would need to read beneath the function signature to understand the requirements for T.
Let’s constrain things a bit
With C++20 we get access to requires.
We will explore requires thoroughly later on,
but for now we use it to improve the readability of getMedianNumber:
template<typename T>
requires std::integral<T> || std::floating_point<T>
// requires std::is_integral_v<T> || std::is_floating_point_v<T>
T getMedianNumber(std::vector<T> values) {
std::sort(values.begin(), values.end());
return values[values.size() / 2];
}
std::vector<std::string> files{"file22.txt", "file11.txt", "file33.txt"};
std::cout << getMedianNumber(files) << std::endl; // 💥 Compilation error
std::vector numbers{0, 9, 5, 7, 3, 6, 2, 8, 1, 4, 10};
std::cout << getMedianNumber(numbers) << std::endl; // 5
Now the “intent” of getMedianNumber is in the function signature.
As a bonus, we get a rather nice compilation error if we try to use an unsupported type as T:
"No operand of the disjunction is satisfied"
...
"requires std::integral<T> || std::floating_point<T>"
Making our first concept
We often want to reuse constraints. Named sets of constraints are called concepts.
Building on the previous example, we can make our own concept to constrain T in a more reusable way:
template<typename T>
concept Number = std::integral<T> || std::floating_point<T>;
// concept Number = std::is_integral_v<T> || std::is_floating_point_v<T>;
template<typename T>
requires Number<T>
T getMedianNumber(std::vector<T> values) {
std::sort(values.begin(), values.end());
return values[values.size() / 2];
}
In the snippet above we defined a concept named Number that checks if T is an integral or
floating-point type.
This allows us to replace the previous constraints with the Number concept, effectively doing
the same thing but in a more readable and reusable way.
You may use concepts to compose more complex constraints or other concepts too.
In fact, std::integral and std::floating_point that are used in this example,
are concepts specified in the standard library.
Think of concepts as something that is satisfied or not.
A concept evaluates to either true or false, like a boolean.
Keep this in mind while we start seeing concepts in more complex scenarios.
Moreover, a trailing requires clause is also possible:
template<typename T>
T getMedianNumber(std::vector<T> values) requires Number<T> // <--- Trailing
{
std::sort(values.begin(), values.end());
return values[values.size() / 2];
}
In this case, the constraint is placed after the arguments list and before the function body.
To be exact, the trailing requires clause is placed after any (optional) trailing return type.
The previous snippet is exactly the same as the previous example, which contained a leading requires clause:
template<typename T> requires Number<T> // <--- Leading requires
T getMedianNumber(std::vector<T> values) {
std::sort(values.begin(), values.end());
return values[values.size() / 2];
}
These two code snippets are equivalent and a matter of preference.
You may use them interchangeably, but in some cases we will examine later,
you may have to use the trailing requires clause.
Focusing on readability
What if I told you we can make things even more readable?
We achieve this by using the concept as a constrained-type template parameter:
template<typename T>
concept Number = std::integral<T> || std::floating_point<T>;
template<Number T> // <--- Concept as type-constraint
T getMedianNumber(std::vector<T> values) {
std::sort(values.begin(), values.end());
return values[values.size() / 2];
}
In the above snippet we have constrained T to satisfy the Number concept
by using it as a constrained-type template parameter.
This way we don’t need to use requires at all and the intent is very clear.
concept vs requires
Before continuing, we should recap the differences between concept and requires:
Use requires to express requirements on the template parameters of a function or class template.
A concept is a named set of such requirements.
Drawing a parallel to more familiar constructs, a concept is to a requires
what a function is to statements.
template<Number T>
requires std::is_integral_v<T> || std::is_floating_point_v<T>
T getMedianNumber1(std::vector<T> values) { /* ... */ }
template<typename T>
requires Number<T>
T getMedianNumber2(std::vector<T> values) { /* ... */ }
You may also think of a concept as a predicate that evaluates to true or false.
To simplify things even further, whenever you see concept think of it as a boolean variable which is either true or false depending on whether the requirements are satisfied or not.
How do you requires?
This part is something I found very confusing and most tutorials seem to brush it off.
If you get this right, then you are well on your way to mastering concepts so here’s where you
need to pay some extra attention.
There are actually two ways to use the requires keyword: with and without curly braces.
template<typename T>
concept Motor = requires(T m) { // <--- `requires` with curly braces
m.start();
m.stop();
};
template<typename T>
requires Motor<T> // <--- `requires` without curly braces
class Car {
// ...
};
When we use requires with curly braces, then requires is followed by (a) optional parentheses
with some “resource” parameters and most importantly, (b) a list of statements.
We haven’t seen this before in this tutorial and we will go through it soon.
The important thing in this way of writing constraints is whether
the list of statements compiles or not.
And what does that mean combined with the requires keyword?
Well, it means the whole expression evaluates to true if the statements compile and false otherwise.
On the other hand, when we use requires without curly braces,
then requires is followed by a boolean expression.
Here you need to remember what we said about concepts earlier: Treat them as booleans.
If the expression after requires without curly braces evaluates to true, then the constraint is “satisfied” or “well-formed”. In other words “it compiles”. If the expression after requires without curly braces evaluates to false, then the constraint is “ill-formed”.
What I want you to start seeing here is that there’s a symmetry between the two ways of using requires.
Don’t worry if you haven’t fully grasped this yet, we will be more thorough later. For now, keep in mind the following:
When you use curly braces, requires evaluates to true or false.
When you don’t use curly braces, requires is well- or ill-formed.
requires without curly braces
Time to dive into the different ways to use requires, starting with the one without curly braces.
When we need to “enable” or “disable” a function based on some “characteristic” of the template type,
we use requires without curly braces, since after it’s evaluated it should be well-formed or ill-formed.
template<typename T>
requires std::is_constructible_v<T, std::string, int>
void createWithStringAndInt() { /* ... */ }
With the risk of repeating myself, here’s what you need to remember:
- A requires without curly braces is followed by a boolean expression.
- If the expression evaluates to
true, then the constraint is satisfied and it is well-formed. It “compiles” in other words. - If the expression evaluates to
false, then the constraint is not satisfied, it is ill-formed and the expression “does not compile”.
The ill-formed expressions may sound strange, but if you are familiar with SFINAE you may remember
that as long as there’s an alternative “that compiles”, the compiler will not report an error.
The same applies here. In the snippet above if we have another overload of createWithStringAndInt()
that happens to work without a T constructible from std::string and int, then that overload
will be chosen and the program will compile.
You may also encounter requires without curly braces followed by parentheses.
We use parentheses when the boolean expression is somewhat more complex.
requires with curly braces
When we need to create a compile-time check that evaluates to true or false
based on some “characteristic” of the template type, we use requires with curly braces.
Remember that requires with curly braces evaluates to true or false so
we often use it to create our own concept.
template<typename T>
concept StringAndIntConstructible = requires(std::string s, int i) {
T{s, i};
};
So what’s up with the curly braces?
- The curly braces should contain a list of statements
- These statements are for evaluation only, they are not executed
- The list of statements may compile or not, in other words they may be well-formed or ill-formed
- In the example above we check if type
Thas a constructor which takes astd::stringand anintas arguments.
- In the example above we check if type
- Think of the list of statements as the “terms” of the “contract” the type
Tshould satisfy- The statements illustrate the different “characteristics” the type should have.
- By the way, the “contract” analogy has nothing to do with C++26 contracts.
- If a statement does not compile, for example type
Tcannot be constructed with astd::stringand anint, then the entirerequiresexpression evaluates tofalse.- Otherwise, if all statements compile, then the entire
requiresexpression evaluates totrue.
- Otherwise, if all statements compile, then the entire
- The curly braces might be preceded by parentheses with some “resource” parameters.
- These parameters are used to check if the type
Tcan be used in a certain way, for example if it can be constructed with certain arguments. - In the above snippet we use a
std::stringcalledsand anintcalledito illustrate the constraintTshould have a constructor that receives these arguments. - You don’t always need these extra “resources” and they are technically not necessary in this example. However, they may be useful in complex scenarios and also make the code more readable.
- These parameters are used to check if the type
- While the parentheses and the curly braces might remind you of a function declaration
and a function body, they are NOT.
- The list of statements are NOT executed, they are only evaluated at compile time.
- The ONLY thing we care about is whether they compile or not.
Recap: With VS without curly braces
It’s been a long journey so far towards becoming comfortable with concepts.
Let’s take a moment to recap the differences between the two ways to use requires.
requires |
Followed by | Evaluates to | Usage |
|---|---|---|---|
| Without curly braces | true or false expression |
Well- or ill-formed statement |
Select overload, enable or disable functionality |
| With curly braces | Well- or ill-formed statement(s) |
true or false expression |
Create constraints or concepts |
When you see requires without curly braces, it means that the expression should either compile or not.
It’s most likely used to enable or disable a function based on some characteristic of the template type,
for example if it is constructible with certain arguments.
Alternatively, it is used to select the right candidate among a set of overloads.
requires without curly braces is followed by a boolean expression.
If the boolean expression evaluates to true, then the constraint is satisfied and it is well-formed.
It “compiles” in other words.
If the boolean expression evaluates to false, then the constraint is not satisfied, it is ill-formed and the expression “does not compile”.
On the other hand, requires with curly braces is a compile-time check
that evaluates to true or false.
It’s often used to create custom concepts.
requires with curly braces is followed by a list of statements and maybe
some parentheses with “resource” parameters.
The list of statements are the “terms” of the “contract” the type should satisfy.
If all statements compile, then the constraint is satisfied and the entire
requires with curly braces expression evaluates to true. Otherwise, it evaluates to false.
“Interfaces” for our template types
By now you should be a bit more comfortable reading concepts and understanding
the different ways to use requires. I dare say that the toughest part is behind us,
but there’s still a lot of fun and important stuff to explore.
// Motor.h
struct Motor {
Motor(int directionPin, int speedPin);
bool start();
bool stop();
};
// Car.h
class Car {
Motor mMotor{5 /* directionPin */, 10 /* speedPin */};
public:
void drive();
};
In the above snippet we have a Car class that depends on Motor.
If we take a look at the Motor’s interface, we know exactly what we need to implement to
create a Motor that can be used by Car. No ambiguity, we shouldn’t have to read how
Car is using Motor to figure out what Motor should look like.
What if, for some reason, we wanted to make Car a class template and allow it to work with any Motor-like type?
How can we communicate that Motor should have a start() and a stop()?
API for template types?
Reviewing Car and how it uses Motor might not be trivial.
template<typename Motor>
class Car {
Motor mMotor{5 /* directionPin */, 10 /* speedPin */};
public:
void drive() {
// A lot of code...
}
};
If Car’s implementation is complex enough, it might be cumbersome to figure out
how the Motor type should look like.
It saddens me to admit, but it is somewhat common to keep recompiling and reading the errors until
figuring out what functionality should the template type expose.
Not very productive, is it?
“Interfaces” for template types without concepts: SFINAE
In the past, we would use SFINAE to create a “trait” that would check if the template type satisfied various criteria. Here is an example:
template<typename T, typename = void>
struct IsMotor : std::false_type {};
template<typename T>
struct IsMotor<T, std::void_t<decltype(std::declval<T>().start()),
decltype(std::declval<T>().stop())>>
: std::true_type {};
template<typename Motor>
class Car {
static_assert(IsMotor<Motor>::value, "Motor needs start and stop");
Motor mMotor;
public:
void drive();
};
IsMotor is a trait that checks if the type T has a start() and a stop().
It can effectively be used as a boolean variable to check if the type Motor
satisfies the requirements to be used in Car.
If we try to instantiate Car<SomeType> where SomeType does not have a start() and a stop(),
then the static assertion will fail and we will get a compilation error
with the message “Motor needs start and stop”.
By the way, if you want to know more about SFINAE and how it works,
check out the following tutorial or YouTube video.
“Interfaces” for template types with concepts
With concepts, we can use a more “natural” way to express requirements for our template types,
without resorting to exotic-looking tricks like SFINAE, std::void_t and std::declval.
Here is how we can express the same requirements for Car’s template type using concepts:
template<typename T>
concept Motor = requires(T m) {
T{int{}, int{}}; // Constructible with two ints
m.start(); // T has a public start method
m.stop(); // T has a public stop method
};
template<Motor M>
class Car {
M mMotor{5 /* directionPin */, 10 /* speedPin */};
public:
void drive();
};
requires as a “contract”
Let’s take a closer look at the Motor concept we defined above:
template <typename T>
concept Motor = requires(T m) {
m.start();
m.stop();
};
To begin with, we have a requires expression with curly braces,
which means that it evaluates to true or false.
The Motor concept is satisfied if an instance of T (i.e. the “motor” implementation we want to use with our Car)
has a start() and a stop() public method.
If there’s no start(), m.start() is ill-formed (i.e. “it cannot compile”), but no compilation error is reported.
Whatever we write inside the curly braces is for “evaluation” only.
Every line within the curly braces is a “term” of the “contract” that the type T should satisfy.
Don’t see them as a sequence of statements or function calls, but rather as “terms in a contract”.
You can check out the full syntax of requires here.
requires requiring…
One example is never enough to get comfortable and I promised you will get comfortable with concepts:
template <typename T>
concept Gyroscope = requires(T g, std::vector<int> params, int frequency) { // 1
T{params}; // 2
g.calibrate(); // 3
{ g.getAngle() } -> std::same_as<double>; // 4
g.setFrequency(frequency); // 5
};
- In the parentheses we have the “resources”/”objects” needed to express the “requirements”. They are not arguments to some function, despite looking like it.
- We require
Tto be constructible with astd::vector<int>. - We require instances of
Tto have a publiccalibrate()method. - We require instances of
Tto have a publicgetAngle()method that returns adouble, i.e. adouble T::getAngle();.
The->syntax refers to the return type of the left-hand side expression, i.e.g.getAngle(). - We require instances of
Tto have a publicsetFrequency(int)method that takes an integer argument and we don’t care about the return type.
The SFINAE equivalent of the above would look something like this:
template<typename T, typename = void>
struct HasGetAngle : std::false_type {};
template<typename T>
struct HasGetAngle<
T, std::enable_if_t<std::is_same<
double, decltype(std::declval<T>().getAngle())>::value>>
: std::true_type {};
Alternatively, in a shorter yet still “exotic” way:
template<typename T>
struct HasGetAngle<T, std::void_t<decltype(std::declval<T>().getAngle())>>
: std::bool_constant<std::is_same<
double, decltype(std::declval<T>().getAngle())>::value> {};
This is a lot of boilerplate code for a “simple” check, isn’t it?
With concepts we can skip all the template metaprogramming “magic” and express constraints
in a way that resembles natural language, without losing any of the SFINAE power.
More requires
When creating a constraint what you mostly need is to know how the type should be used. Instead of declaring the function signatures in an interface of a “normal” class, you give examples of how the type can be called, operated on etc.
template<typename T>
concept MyBigConcept = requires(T a, T b, std::ostream& out) {
a + b; // Addable with its own type
a++; // Incrementable
{ a == b } -> std::same_as<bool>; // Equality comparable
typename T::inner; // T::inner is a type (exists)
{ out << a } -> std::same_as<std::ostream&>; // Streamable to std::ostream
requires std::integral<typename T::value_type>; // T::value_type satisfies std::integral
{ a.size() } -> std::integral; // Return type satisfies other concept
{ T::Instances } -> std::same_as<std::size_t>; // T::Instances static and std::size_t
a.id; // `id` is a public member variable
};
Choosing the right candidate (à la SFINAE)
We can use concepts to select the right overload. This allows us to follow different code paths based on properties of the template type:
template<typename Robot>
void handleEnemies(Robot) { std::cout << "I surrender!\n"; }
template<typename Robot>
requires HasBullets<Robot>
void handleEnemies(Robot r) { r.shootBullets(); }
template<HasMissiles Robot>
void handleEnemies(Robot r) { r.shootMissiles(); }
struct RobotA { void shootBullets() { std::cout << "Bang!\n"; } };
struct RobotB { void shootMissiles() { std::cout << "Shooosh!\n"; } };
struct RobotC {};
handleEnemies(RobotA{}); // "Bang!"
handleEnemies(RobotB{}); // "Shooosh!"
handleEnemies(RobotC{}); // "I surrender!"
There are three overloads of handleEnemies(). A base-case, one with requires without curly braces and one
where the concept is used as a constrained-type template parameter.
The compiler will choose the most specialized overload.
requires without curly braces is well-formed if satisfied and ill-formed otherwise.
The concept as a constrained-type template parameter behaves in the same way.
So if HasBullets or HasMissiles are not satisfied, the base-case overload will be chosen
and no compilation error will be reported.
That being said, if there is more than one eligible overload,
e.g. there’s a type that satisfies both HasBullets and HasMissiles,
then the program will fail to compile with an error about ambiguity.
Specializing member functions
In the previous example we saw how to select the right overload of a (free) function based on properties of the template type. We can do the same thing with member functions of a class template:
template<typename Motor>
concept HasOdometer = requires(Motor m) {
m.getPulses();
};
template<typename Motor>
struct Car {
void drive() { std::cout << "Drive\n"; }
void drive() requires HasOdometer<Motor> {
std::cout << "Drive with cruise control\n";
}
};
Again, the most specialized overload will be chosen by the compiler.
In this example we can also see how the trailing requires clause makes things
more convenient and readable.
This new syntax beats the SFINAE equivalent by miles.
Try to imagine doing this with SFINAE and you will see what I mean.
if constexpr and requires
We saw that requires with curly braces evaluates to true or false
based on whether the statements compile or not.
This means that we can use it as a conditional. Let’s see a peculiar example,
where we create a constraint on the fly, with if constexpr:
template<typename T>
void print_info(T value) {
if constexpr (requires(int i) { value.foo(i); }) {
std::cout << "T has foo(int) member function\n";
} else if constexpr (requires { value.bar(); }) {
std::cout << "T has bar() member function\n";
} else {
std::cout << "T has neither foo(int) nor bar() member functions\n";
}
}
Here we check whether T has various member functions
in an if-else-if-else statement and only compile the relevant code for each case.
While I wouldn’t recommend this approach for complex constraints,
since it feels somewhat “cramped”, it gets the job done.
You can adopt this approach for simple checks and immediately use constraints
without having to define a (named) concept first.
If you have been following along, you may have noticed we use requires in two different ways:
With and without curly braces.
Once you master the differences between the two, you will realize we can use them together in various “creative” ways.
However, occasionally it is not obvious what the correct usage is. Let’s take a look at the following example:
template<typename T>
constexpr void print_type_info(const T& value) {
if constexpr (requires { std::is_integral_v<T>; }) {
std::cout << "Value is integral: " << value << std::endl;
} else {
std::cout << "Value is not integral" << std::endl;
}
}
print_type_info(5);
print_type_info(3.14);
print_type_info(std::string{"Hello"});
What do you think will be printed out?
On a first look, we may think that the first print_type_info call will print that the value is integral,
while the other two that it is not.
However, the actual output is:
Value is integral: 5
Value is integral: 3.14
Value is integral: Hello
This happens because requires { std::is_integral_v<T>; } is always satisfied which appears
counterintuitive at first.
On a closer look, we can see that the statement std::is_integral_v<T>; is well-formed for any type T.
If we execute std::is_integral_v<T>, it evaluates to true or false but that’s not very useful is it?
The statement is always well-formed whatever T is, so the relevant requires expression is a constraint that is always satisfied.
(Avoid) Concepts that are always satisfied
Constraints that are always satisfied are as useful as a condition that’s always the same. It might as well not be there at all, right?
template<typename T>
concept AlwaysSatisfied1 = true;
template<typename T>
concept AlwaysSatisfied2 = requires { false; };
template<typename T>
concept AlwaysSatisfied3 = requires(T t) {
std::is_integral_v<T>;
std::is_floating_point_v<T>;
};
static_assert(AlwaysSatisfied1<int>); // Hardcoded to true
static_assert(AlwaysSatisfied2<int>); // `false;` is a valid statement
static_assert(AlwaysSatisfied3<int>); // `true;` and `false;` are valid
AlwaysSatisfied1 is the obvious case here, since it’s hardcoded to true. The next two are more interesting.
AlwaysSatisfied2 is satisfied because false; is a valid statement. So what if it’s false?
requires with curly braces cares about whether the statements are well-formed or not and evaluates to a boolean.
false is well-formed so AlwaysSatisfied2 always evaluates to true.
Finally, AlwaysSatisfied3 is satisfied due to the same reason as the one we described in the previous snippet.
The statements std::is_integral_v<T>; and std::is_floating_point_v<T>; are well-formed for any type T.
Remember that we are not executing these statements, but only examining their validity.
Here are some more examples:
template<typename T>
concept Integral = requires {
std::integral<T>; // 1
requires std::integral<T>; // 2
std::is_integral_v<T>; // 3
{ T{} } -> std::integral; // 4
};
- This is always a valid statement, regardless of
T. - This is a
requireswithout curly braces which becomes invalid ifTdoes not satisfystd::integralso it’s OK to have it here. - This is always a valid statement, regardless of
T. - This statement is invalid if
Tcannot be default constructed or if aTinstance cannot be converted to an integral type, so it’s OK to have it here.
static_assert and concepts
Here’s another example to remind you that the statements inside requires with curly braces
are executed neither at runtime nor at compile time, but only evaluated for their validity:
template<typename T>
T loopback(T value) {
static_assert(!std::is_same_v<T, int>, "Never call with int");
return value;
}
template<typename T>
concept CanLoopBack = requires { loopback(T{}); };
int main() {
// std::cout << loopback(1) << std::endl; // Won't compile, static assert fails
std::cout << std::boolalpha << CanLoopBack<double> << std::endl;
std::cout << std::boolalpha << CanLoopBack<int> << std::endl;
return 0;
}
CanLoopBack<T> is always satisfied, which may be surprising at first.
This is because the statement loopback(T{}); is well-formed for any type T, even for int.
The static_assert inside loopback despite causing a compilation error if we try to call it
with an int, it does not affect the validity of the statement loopback(T{}); in the requires expression.
loopback(T{}); effectively checks only whether T can be default constructed and passed to loopback().
requires { requires <true|false> }
Remember the previous example with print_type_info()
where we had a requires with curly braces that was always satisfied?
We can fix it by introducing a somewhat confusing requires requires syntax:
template<typename T>
constexpr void print_type_info(const T& value) {
if constexpr (requires { requires std::is_integral_v<T>; }) {
std::cout << "Value is integral: " << value << std::endl;
} else {
std::cout << "Value is not integral" << std::endl;
}
}
We have a requires with curly braces that contains a requires without curly braces.
The inner requires (without curly braces) is a constraint that is well-formed if the boolean expression
that follows evaluates to true and ill-formed otherwise.
The outer requires (with curly braces) evaluates to true if the inner
requires is well-formed and false otherwise.
As a result, the if constexpr condition is only satisfied if T is an integral type,
which is the intended behavior.
requires requires { statements...; }
For the sake of completeness, we can have a requires with curly braces
containing a requires with curly braces:
template<typename Container>
requires requires(Container a, Container::value_type v1, Container::value_type v2) {
{ a.begin() } -> std::input_iterator;
{ a.end() } -> std::sentinel_for<decltype(a.begin())>;
{ a.size() } -> std::same_as<std::size_t>;
{ v1 < v2 } -> std::same_as<bool>;
}
void print_sorted(Container& c) { /* ... */ }
This is an example of how to check if Container conforms to the specific API, in-place,
without having to define a named concept first.
The inner requires (with curly braces) is satisfied if all statements “compile” and evaluates to true.
The outer requires (without curly braces) is well-formed if the inner requires evaluates to true.
auto function arguments
With C++20 we can use auto in function parameters when we want a quick way to parameterize a function
without the boilerplate of the full template syntax:
bool isMagicNumber1(auto value) {
return value == 42;
}
// Equivalent to:
template<typename T>
bool isMagicNumber2(T value) {
return value == 42;
}
If you have tried to use auto in function parameters the compiler might have asked you to include
-fconcepts and -std=c++20 flags, which is a bit confusing
since we don’t use the concepts or requires keywords.
The auto keyword in function parameters was actually introduced in C++20 as part of the concepts feature
and it provides another way to constrain a function argument.
template<typename T>
concept Integral = std::integral<T>;
bool isMagicNumber1(Integral auto value) {
return value == 42;
}
// Equivalent to:
template<Integral T>
bool isMagicNumber2(T value) {
return value == 42;
}
In the last snippet we have constrained the value parameter to satisfy the Integral concept.
Note the syntax: We have the Integral concept followed by auto and then the parameter name.
You cannot do bool isMagicNumber1(Integral value), i.e. skip the auto, since Integral is not a type.
It might have looked nicer in this specific case, but would have been very confusing otherwise.
isMagicNumber2 demonstrates one equivalent way to write the same thing using the concept as a constrained-type template parameter.
Concepts with multiple types
So far we have only seen concepts that express constraints on a single type, but there’s no reason why we can’t do the same thing with multiple:
template<typename Motor, typename Odometer>
concept CompatibleOdometry = requires(Motor m, Odometer o) {
m.attach(o);
};
template<typename Motor, typename Odometer>
requires CompatibleOdometry<Motor, Odometer>
class Smartcar {
public:
Smartcar(Motor left, Motor right, Odometer odometer) { /* ... */ }
};
There isn’t anything fundamentally different with this example.
CompatibleOdometry checks if a Motor object can be attached to an Odometer.
The Smartcar class is constrained to only work with compatible Motor and Odometer types,
by specifying both types as template parameters: requires CompatibleOdometry<Motor, Odometer>.
1st argument deduction
We previously saw that an argument to a constraint can be deduced by the compiler, for example:
template<typename T>
concept Container = requires(T container) {
{ container.size() } -> std::integral; // The return type must be an integral type
};
In the above snippet, we check if container.size() returns an integral type.
We don’t need to manually pass the return type to std::integral since the compiler deduces it.
std::integral is a built-in concept that checks if the type passed is an integral type.
For example, std::integral<int> is satisfied and evaluates to true.
What happens if we have a constraint with multiple arguments? The first one is deduced.
template<typename Motor, typename Odometer>
concept CompatibleOdometry = requires(Motor m, Odometer o) { m.attach(o); };
template<typename MotorFactory, typename Odometer>
concept CompatibleMotorFactory = requires(MotorFactory mf) {
{ mf.create() } -> CompatibleOdometry<Odometer>; // The return type of create() is fed into CompatibleOdometry
};
struct Odometer {};
struct GoodMotor {
void attach(Odometer) {}
};
struct BadMotor {
void connect(Odometer) {}
};
struct GoodMotorFactory {
GoodMotor create() {}
};
struct BadMotorFactory {
BadMotor create() {}
};
int main() {
std::cout << CompatibleMotorFactory<GoodMotorFactory, Odometer> << std::endl; // true
std::cout << CompatibleMotorFactory<BadMotorFactory, Odometer> << std::endl; // false
return 0;
}
In the above snippet, CompatibleMotorFactory checks if a MotorFactory can create a Motor compatible with a given Odometer.
The first argument of CompatibleOdometry is deduced by the compiler to be the return type of mf.create().
This is a powerful way to compose concepts and create complex constraints.
Other examples of deducing the first argument of a constraint are std::same_as, std::derived_from and std::convertible_to.
Since we want you to become comfortable with concepts, here is another useful example of deducing the first argument:
template<typename T, typename U, typename V>
concept Either = std::same_as<T, U> || std::same_as<T, V>;
auto onlyWorksForIntOrString = [](Either<int, std::string> auto x) { return x; };
std::cout << onlyWorksForIntOrString(42) << '\n'; // OK
std::cout << onlyWorksForIntOrString(std::string{"hello"}) << '\n'; // OK
std::cout << onlyWorksForIntOrString("hello") << '\n'; // 💥 const char* is not a std::string or int
Either is satisfied if T is the same as U or V.
In onlyWorksForIntOrString, the type of x is constrained to satisfy Either<int, std::string>.
In other words, Either’s T corresponds to the type of x and is deduced by the compiler.
U and V are explicitly specified as int and std::string.
Concepts with generic lambdas (no template parameter list)
In the last example, we got a sneak peek of how to use concepts with lambdas so it is only proper to dig a bit deeper into it.
auto stopIfObstructed = [](auto& car, auto& sensor)
requires requires {
{ sensor.isObstructed() } -> std::convertible_to<bool>;
car.stop();
}
{
if (sensor.isObstructed()) { car.stop(); }
};
It looks strange but we need to go through it on our quest to get comfortable with concepts.
We have defined a generic lambda that sets some constraints on the parameters it takes.
One way to create a generic lambda is with auto parameters, a cool feature supported as of C++14.
We have implemented the particular lambda to only be callable with a car instance that has a stop()
method and a sensor that has an isObstructed() method that returns something convertible to bool.
While creating a dedicated concept for this would improve readability,
the example does not contain anything we haven’t seen so far.
Readability aside, we can use the auto parameter variable names directly in the requires with curly braces expression.
To address the elephant in the room, there are two parts that look daunting on a first sight:
- The
requires requiressyntax, which we have already seen in the previous section. We need it to specify a constraint without creating a named concept first. - The two “blocks” of the lambda. One for specifying the constraints, after the
requires requiresand one for the lambda body. They look like two function bodies, but they are not. The first one is for evaluation only, while the second one is the actual body of the lambda.
Concepts with generic lambdas (with template parameter list)
Lambda expressions can also have a template parameter list, making them implicitly generic.
Using a lambda with a template parameter list, compared to a lambda with auto arguments,
might give you more control over the template parameter types.
That being said, in both cases you can constrain the parameters with concepts and requires:
auto stop1 = []<typename Car, typename Sensor>
requires CanDetectObstruction<Sensor> && CanStop<Car>
(Car & car, Sensor & sensor) {
if (sensor.isObstructed()) { car.stop(); }
};
auto stop2 = []<CanStop Car, CanDetectObstruction Sensor>(Car& c, Sensor& s) {
if (s.isObstructed()) { c.stop(); }
};
In the snippet we see two generic lambdas with template parameter lists that are constrained with concepts.
stop1 has a trailing requires clause that uses the CanDetectObstruction and CanStop concepts.
stop2 on the other hand uses the concepts as constrained-type template parameters and achieves the same effect.
Compared to the auto parameters version, while this syntax is more verbose, it gives us more control
by allowing us to use the type names with existing concepts, something that would have been somewhat more
verbose with auto parameters, since we would have to decltype
the parameters to get their types and feed them into the concepts:
auto stopIfObstructedAlt = [](auto& car, auto& sensor)
requires CanStop<decltype(car)> && CanDetectObstruction<decltype(sensor)>
{
if (sensor.isObstructed()) { car.stop(); }
};
Concepts with variadic templates
Once you think you have seen it all, concepts combined with variadic templates come to surprise you.
template<typename T, typename... Ts>
concept IsAnyOf = (std::is_same_v<T, Ts> || ...);
auto l = [](IsAnyOf<int, double, Foo> auto x) { return x; };
std::cout << l(42) << std::endl;
std::cout << l(3.14) << std::endl;
std::cout << l("Hello") << std::endl; // 💥 Compilation error
IsAnyOf is a concept that checks if T is the same as any of the types in Ts....
The lambda l is constrained to only be callable with an argument that is an int, a double or a Foo.
The main source of complexity in this example is the variadic template parameter pack and the fold expression,
rather than the concepts themselves.
Concepts with variadic templates and if constexpr
Throwing if constexpr into the mix, allows us to get a bit more comfortable with concepts.
To demonstrate the combo, we create an interesting constraint that allows us to follow a different code path
based on “properties” of the types in the parameter pack:
template<typename... Ts>
concept HasAlcohol = (requires(Ts ts) { ts.fermentedOrDistilled(); } || ...);
template<typename... Ingredients>
auto makeDrink(Ingredients&&... ingredients) {
if constexpr (HasAlcohol<Ingredients...>) {
return Cocktail{std::forward<Ingredients>(ingredients)...};
} else {
return Mocktail{std::forward<Ingredients>(ingredients)...};
}
}
HasAlcohol is satisfied if any of the types in Ts... has a fermentedOrDistilled() method.
When that is the case, makeDrink() returns a Cocktail, otherwise a Mocktail.
In other words, we use the HasAlcohol constraint to not only select the right code path,
but also determine the return type of the function template.
I am sure you can think of creative ways to use this by creating readable and type-safe APIs,
despite using “advanced” features.
Concepts with variadic templates and requires
For completeness, we can refactor our makeDrink() example to let the compiler choose the right overload,
instead of using if constexpr:
template<typename... Ts>
concept HasAlcohol = (requires(Ts ts) { ts.fermentedOrDistilled(); } || ...);
template<typename... Ingredients>
requires HasAlcohol<Ingredients...>
auto makeDrink(Ingredients&&... ingredients) {
return Cocktail{std::forward<Ingredients>(ingredients)...};
}
template<typename... Ingredients>
requires(!HasAlcohol<Ingredients...>)
auto makeDrink(Ingredients&&... ingredients) {
return Mocktail{std::forward<Ingredients>(ingredients)...};
}
This example is similar to what we have previously seen, where requires without curly braces
is used to select the right overload.
This approach might be preferable to the if constexpr one if the different code paths are complex
to warrant separate functions.
Takeaways
That was a long ride but I hope you are now more comfortable with concepts.
If you made it this far, you should be able to handle most of the common use cases of concepts
and understand the different ways to use requires.
Once you get used to the new syntax and the different ways to use requires,
the main challenge will be the inherent complexity of templates, rather than the concepts themselves.
To summarize:
- Concepts are named sets of constraints/requirements on template types
conceptis torequireswhat a function is to a statement
- Two types of
requireswhich can be confusing:requireswithout curly braces- Expects a boolean expression, evaluates to valid or ill-formed
requireswith curly braces- Expects a block of statements, evaluates to
trueorfalse
- Expects a block of statements, evaluates to
- Constrain template parameters to simplify code and provide better error messages
- Use concepts to create “interfaces” for template classes & methods
- Skip reading code or compiler errors to find the right type to use
- Avoid cryptic and verbose SFINAE constructs
static_assertis still useful for providing custom error messages

