C++20 introduced a new feature called concepts that gives us a more direct and powerful way of specifying constraints on template parameters. This feature can help prevent errors and make code more readable by limiting what types can be used with a given template. Some of the advantages include:
In this lesson, we’ll introduce concepts with some examples from the standard library. We’ll then show the four main ways we can use concepts with our templates.
Finally, we’ll show how we can create our own concepts. We’ll start with some basic examples and gradually ramp up the complexity to specify more and more advanced requirements.
The standard library includes a collection of pre-defined concepts we can use. They’re available from the <concepts>
 header:
#include <concepts>
For example, the std::integral
concept can tell us if a type is an integer. At their most basic usage, concepts look similar to type traits. We pass the type (and depending on the concept, possibly some additional arguments) and we get a boolean value representing whether or not the type meets the requirements of the concept.
This all happens at compile time
#include <iostream>
#include <concepts>
int main() {
if constexpr (std::integral<int>) {
std::cout << "int is an integral";
}
if constexpr (!std::integral<float>) {
std::cout << "\nfloat isn't";
}
}
int is an integral
float isn't
Note, concepts are also available in multiple standard library locations. Notably, this includes the <ranges>
and <iterators>
headers, which we’ll introduce later in this course in our chapters on containers and algorithms
There are four main ways we can use concepts within our code.
Where concepts deviate from type traits is that the language has built-in syntactic support to make them easier to use. This most notably applies when we want to use a concept to constrain the types that can be used by a template.
For example, we can replace typename
with a concept to constrain a template parameter.
So, if we wanted to ensure our template type was an integer, we no longer need to manually create static assertions. Instead, we can just replace typename
with a concept in our template parameter list.
So, instead of writing this:
template <typename T>
void SomeFunction(T x) {
// ...
}
We can instead write this:
template <std::integral T>
void SomeFunction(T x) {
// ...
}
We’re now fully documenting our type requirements, and the compiler will generate meaningful error messages if anyone tries to use our template in a way we didn’t intend:
#include <concepts>
template <std::integral T>
T Average(T x, T y) {
return (x + y) / 2;
}
int main() {
Average(1, 2);
Average(1.5, 2.2);
}
the associated constraints are not satisfied
the concept 'std::integral<double>' evaluated to false
Similar to the example we showed using std::enable_if
in the previous lesson, concepts allow us to route function calls to different templates, in a much clearer way:
#include <iostream>
#include <concepts>
template <std::integral T>
T Average(T x, T y) {
std::cout << "Using integral function\n";
return (x + y) / 2;
}
template <std::floating_point T>
T Average(T x, T y) {
std::cout << "Using floating point function";
return (x + y) / 2;
}
int main() {
Average(1, 2);
Average(1.5, 2.2);
}
Using integral function
Using floating point function
We can also use identical syntax when creating template classes:
#include <concepts>
template <std::integral T>
class Container {
T Contents;
};
int main() {
Container<int> Stuff;
Container<float> MoreStuff;
}
'Container': the associated constraints are not satisfied
the concept 'std::integral<float>' evaluated to false
requires
KeywordThe addition of concepts also came with a new piece of syntax - the requires
keyword. This keyword is used in a few different ways, which we’ll cover in this lesson.
Below, we use the requires
keyword in a function template. It has access to the types our template is using, and will return true
if the template can handle those types.
We could rewrite our previous example using requires
like this:
#include <iostream>
#include <concepts>
template <typename T>
requires std::integral<T>
T Average(T x, T y) {
std::cout << "Using integral function\n";
return (x + y) / 2;
}
template <typename T>
requires std::floating_point<T>
T Average(T x, T y) {
std::cout << "Using floating point function";
return (x + y) / 2;
}
int main() {
Average(1, 2);
Average(3.4, 5.3);
}
Using integral function
Using floating point function
The requires
method is more verbose than simply replacing typename
, but it gives us some more options. For example, we can use boolean logic. In the below example, we allow our type to match one of two concepts, using a requires
clause and the ||
 operator:
#include <concepts>
template <typename T>
requires std::integral<T> ||
std::floating_point<T>
T Average(T x, T y) {
return (x + y) / 2;
}
int main() {
Average(1, 2);
Average(3.4, 5.3);
}
We’re also not restricted to just using concepts within requires
statements - we can use other compile-time techniques too. Below, we use the std::is_base_of
type trait from the previous lesson:
#include <concepts>
class Player {};
class Monster {};
class Goblin : public Monster {};
template <typename T>
requires std::is_base_of_v<Player, T> ||
std::is_base_of_v<Monster, T>
void Function(T Character) {}
int main() {
Function(Goblin{});
}
Note, most standard library-type traits have equivalent concepts. For example, the std::derived_from
concept covers the same use cases as the std::base_of
type trait, albeit the order of the two types is inverted.
We can use a requires statement with class templates in the way we might expect:
#include <concepts>
template <typename T>
requires std::integral<T>
class Container {
T Contents;
};
When creating function templates, we have the option of using a requires
statement in a slightly different way. It can be placed between the function signature and the function body, like this:
template <typename T>
T Average(T x, T y)
requires std::integral<T>
{
return (x + y) / 2;
}
The final way we can use concepts is within abbreviated function templates. These allow us to create function templates by setting one or more parameter types to auto
:
auto Average(auto x, auto y) {
return (x + y) / 2;
}
Where we want to implement concepts here, we insert the concept before the auto
 type:
auto Average(std::integral auto x,
std::integral auto y) {
return (x + y) / 2;
}
We are free to combine the previous techniques as we see fit. Below, we use a constrained template parameter to specify requirements for the first type, and a requires
statement to restrict the second type.
For the template to be eligible for a given function call, all the requirements need to be met.
template <std::integral TFirst, typename TSecond>
requires std::integral<TSecond> ||
std::floating_point<TSecond>
void Function(TFirst x, TSecond y){}
Naturally, we’re not restricted to just using the standard library concepts. We can use concepts from any source, or write our own.
A basic concept looks like this
#include <concepts>
template <typename T>
concept Integer = std::integral<T>;
We can break it down into four components:
concept
keywordtrue
if the concept is met, or false
otherwise.We can then use our concept in any of the usual ways:
template <Integer T>
T Average(T x, T y) {
return (x + y) / 2;
}
The previous concept is effectively just re-implementing the std::is_integral
concept under a different name, but we can of course get more complex. The following concept combines two standard library concepts and will be satisfied if a type matches either:
#include <concepts>
template <typename T>
concept Numeric =
std::integral<T> || std::floating_point<T>;
template <Numeric T>
T Average(T x, T y) {
return (x + y) / 2;
}
More complex requirements can be specified by an alternative form of the requires
syntax, which looks somewhat like a function. Within the body of this syntax, we can write code that mostly looks like normal function code:
#include <concepts>
template <typename T>
concept Averagable =
requires(T x, T y) {
(x + y) / 2;
};
But, what we’re actually doing here is specifying requirements. In the above example, what we’re saying is for a type to satisfy our concept, it must meet two requirements.
Firstly, an object of that type must be addable to another object of that same type.
More specifically, the type T
must implement an +
operator, where the second operand is also a T
.
Secondly, the object returned from that operation must be divisible by 2
. That is, it must return a type that implements the /
operator, where the second operand is an int
, or convertible to an int
.
If a type meets those requirements, it is Averagable
, otherwise, it is not.
1#include <string>
2
3template <typename T>
4concept Averagable =
5requires(T x, T y){
6 (x + y) / 2;
7};
8
9template <Averagable T>
10T Average(T x, T y){ return (x + y) / 2; }
11
12int main(){
13 Average(3, 5);
14
15 std::string A{"Hello"};
16 std::string B{"World"};
17 Average(A, B);
18}
19
Prior to concepts, something like this would have been extremely difficult to set up, requiring advanced template metaprogramming. Now, it’s fairly straightforward, and with most compilers, the error output is much better than we’d be able to achieve previously. It may look something like this:
main.cpp(17,3)
'Average': no matching overloaded function found
could be 'T Average(T,T)'
the associated constraints are not satisfied
the concept 'Averagable<std::string>' evaluated to false
binary '/': 'std::string' does not define this operator
It’s telling us on line 17 of main.cpp
, no function was found that could satisfy that specific call to Average
.
It correctly identified our template function as a candidate, but ruled it out because of the type constraints we added.
It tells us exactly which constraint ruled the template out - our Averagable
concept, when passed a std::string
returned false
And it even tells us exactly why the concept returned false
- because the std::string
type does not define the /
operator we specified as a requirement.
We can write concepts that require our types to have specific methods and parameters. We just need to refer to these methods and fields within our requires
block, using the normal syntax such as the .
and ()
 operators.
Below, we write a concept that checks if a type has a Render
method and a hasRendered
 variable:
1template <typename T>
2concept Renderable =
3requires(T Object){
4 Object.Render();
5 Object.hasRendered;
6};
7
8template <Renderable T>
9void Render(T Object){ Object.Render(); }
10
11class Tree {
12public:
13 bool hasRendered;
14 void Render(){}
15};
16
17class Rock {
18public:
19 void Render(){}
20};
21
22int main(){
23 Render(Tree{});
24 Render(Rock{});
25}
26
main.cpp(24,3): 'Render': no matching overloaded function found
could be 'T Render(T)'
the associated constraints are not satisfied
the concept 'Renderable<Rock>' evaluated to false
'hasRendered': is not a member of 'Rock'
see declaration of 'Rock'
Later in this lesson, we’ll see how we can test the type of class variables, as well as the return type and parameter type of class methods.
For now, note we can also combine requires
statements with other boolean expressions in our concepts. Below, we check if our type derives from the Object
class, and that it has both a Render
and Animate
 method:
template <typename T>
concept Animatable =
std::derived_from<T, Object> &&
requires(T Object){
Object.Render();
Object.Animate();
};
We can’t use ||
operators within requires
blocks, but we can have multiple blocks, separated by ||
. Below, we check if our type has either a Render
or a Draw
 method:
template <typename T>
concept Renderable = requires(T Object){
Object.Render();
} || requires(T Object){
Object.Draw();
};
Where we wish to ensure the method accepts a type of argument, we can do that by passing arguments of the appropriate type to the method “call” within the requires
 block.
Below, we require that our type has a Render
method and that that method accepts an int
. The choice of 2
is arbitrary - we could have passed any value of the appropriate type:
template <typename T>
concept Renderable =
requires(T Object) {
Object.Render(2);
};
Generally, we’d want to document what this argument actually is. We can do that by adding it as a named parameter parameter to the requires
 block.
For example, if we expect this integer is likely to represent some a size, our concept might look something like this:
template <typename T>
concept Renderable =
requires(T Object, int Size) {
Object.Render(Size);
};
Falling foul of this constraint would emit an error message similar to what is shown below:
1template <typename T>
2concept Renderable =
3requires(T Object, int Size){
4 Object.Render(Size);
5};
6
7template <Renderable T>
8void Render(T Object, int Size){
9 Object.Render(Size);
10}
11
12class Tree {
13public:
14 void Render(int Size){}
15};
16
17class Rock {
18public:
19 void Render(){}
20};
21
22int main(){
23 Render(Tree{}, 42);
24 Render(Rock{}, 42);
25}
26
main.cpp(24,3): 'Render': no matching overloaded function found
could be 'void Render(T,int)'
the associated constraints are not satisfied
the concept 'Renderable<Rock>' evaluated to false
'Rock::Render': function does not take 1 arguments
while trying to match the argument list '(int)'
In this example, we only show how we can assert a parameter type against a single specific type, such as int
. Later in this lesson, we show a more advanced example where we can check if a parameter type satisfies a concept, or any other compile-time tests.
The syntax to test a method’s return value within a concept is a little more complex. It looks like this:
template <typename T>
concept Renderable = requires(T Object) {
{ Object.Render() } -> std::integral;
};
To break this down, we wrap our method “call” in braces, {
and }
. We then add an arrow, ->
. Finally, we provide a concept we require the return value to satisfy.
Commonly, we’ll want to assert that the return type matches a specific type, or is convertible to a specific type. The C++ specification requires us to provide a concept here rather than a type, but the standard library’s same_as
and convertible_to
concepts can cover our needs.
Below, we are requiring render
to return an int
:
template <typename T>
concept Renderable = requires(T Object) {
{ Object.Render() } -> std::same_as<int>;
};
Alternatively, we can loosen our requirements, and ask it return something convertible to an int
:
template <typename T>
concept Renderable = requires(T Object) {
{ Object.Render() } -> std::convertible_to<int>;
};
Let's see a full example:
1#include <concepts>
2
3template <typename T>
4concept Renderable = requires(T Object){
5 { Object.Render() } -> std::same_as<int>;
6};
7
8template <Renderable T>
9void Render(T Object){ Object.Render(); }
10
11class Tree {
12public:
13 int Render(){ return 42; };
14};
15
16class Rock {
17public:
18 float Render(){ return 3.14; };
19};
20
21int main(){
22 Render(Tree{});
23 Render(Rock{});
24}
25
main.cpp(23,3): 'Render': no matching overloaded function found
could be 'void Render(T)'
the associated constraints are not satisfied
the concept 'Renderable<Rock>' evaluated to false
the concept 'std::same_as<float,int>' evaluated to false
'float' and 'int' are different types
Let's imagine we have a template class, and that class has methods that work on the templated type:
template <typename ArgType>
class Object {
public:
void Render(ArgType Arg){}
};
We have a template function that calls the Render
method on incoming objects and passes an argument to that object. Both the type of the object, and the type of the argument, are template types. We’ll call the object type T
, and the type to be passed to the Render
function will be called ArgType
:
template <typename T, typename ArgType>
void Render(T Object, ArgType Arg){
Object.Render(Arg);
}
int main(){
Object<int> MyObject;
Render(MyObject, 5);
}
How can we write a concept to specify the requirements of T
in this situation? We know that T
requires a Render
method, and that method needs to accept an argument of type ArgType
. But ArgType
is a template type - we don’t know what it will be.
Fortunately, as we’ve seen with some of the standard library examples, concepts are not restricted to just a single template parameter.
In this case, we can create a concept that accepts two template parameters. Our type T
will be the first template parameter, and we can have a second template parameter to receive the Render
method’s required argument type.
Our concept can look something like this:
template <typename T, typename ArgType>
concept Renderable = requires(
T Object, ArgType Arg
){
Object.Render(Arg);
};
This gives us a versatile, reusable concept that lets us check if a type has a Render
method, and that that method accepts an argument. But the type of that argument is not fixed - it is something that we can change any time we want to use the concept:
// Does Tree have a Render method that accepts an int?
Renderable<Tree, int>
// Does Rock have a Render method that accepts a std::string?
Renderable<Rock, std::string>
// Template arguments can themselves be template types
Renderable<TemplateTypeA, TemplateTypeB>
The last point is crucial - the arguments passed to our concepts can be template types, so the compiler can take care of it.
This allows us to pass the types through from our template function to our concept, and the concept then reports back to tell our template if the types meet the requirements.
We can do this using a requires
statement, as below:
template <typename T, typename ArgType>
requires Renderable<T, ArgType>
void Render(T Object, ArgType Arg){
Object.Render(Arg);
}
With those changes, our code should still compile, and we’ve now specified and documented what requirements we have for our template function.
The previous example used the requires
clause to invoke a concept that had multiple arguments.
requires Renderable<T, ArgType>
This perhaps is the clearest way to use the concept in this case, but we could have written the previous example like this instead:
template <typename ArgType, Renderable<ArgType> T>
void Render(T Object, ArgType Arg){
Object.Render(Arg);
}
In the first line of this example, we have changed the order of the template parameters, such that ArgType
comes first. This is not particularly significant - it was simply necessary because ArgType
needs to be defined before we use it, as in Renderable<ArgType>
The order of the template arguments and the order of the function arguments does not need to be the same.
It’s likely less obvious what is going on in this example, but it highlights something about concept template arguments that is important to understand.
When a concept has a single template parameter, such as std::integral
, and we use it as a standalone expression, we need to provide the type we want to test as a template argument, such as std::integral<int>
:
template <typename T>
requires std::integral<T>
void Function(T x){}
However, when the concept is used to replace typename
within a constrained template parameter, or prepended to auto
within an abbreviated function template, the template argument is substituted into the concept for us. We don’t need to provide it - we can just use std::integral
or, less commonly, std::integral<>
template <std::integral T>
void FunctionA(T x){}
// Including <> is an option, but rarely used
template <std::integral<> T>
void FunctionB(T x){}
// Abbreviated function template
void FunctionC(std::integral auto x){}
This idea extends to concepts that have 2 (or more) template parameters in a similar way. When used in isolation, we need to provide all the parameters:
class BaseClass {};
template <typename T>
requires std::derived_from<T, BaseClass>
void Function(T x){}
When used to replace typename
or prepended to auto
, the first parameter is automatically inferred, and we just need to provide the remaining parameters:
class BaseClass {};
template <std::derived_from<BaseClass> T>
void FunctionA(T x){}
void FunctionB(
std::derived_from<BaseClass> auto x){}
This has implications for how we design our own concepts. If our concept has multiple template parameters, the type that we want to inspect should be the first one. If it’s not, our concept will not be usable as a constrained template parameter or abbreviated function template.
In those contexts, the type of argument to the function becomes the first parameter of our concept - the developer using our concept cannot change that, so it makes our concept less useful.
requires
We can nest additional requires
expressions within other requires
statements. The most common use case of this is when we want to check that a type contains a variable, and we also want to apply constraints to the type of that variable.
In the following example, our Renderable
constraint is satisfied if the type has a Render
method, and additionally has an isRendered
field that is a boolean:
1#include <concepts>
2
3template <typename T>
4concept Renderable =
5requires(T Object){
6 Object.Render();
7 requires std::same_as<
8 decltype(Object.hasRendered),
9 bool>;
10};
11
12template <Renderable T>
13void Render(T Object){ Object.Render(); }
14
15class Tree {
16public:
17 bool hasRendered;
18 void Render(){}
19};
20
21class Rock {
22public:
23 std::string hasRendered;
24 void Render(){}
25};
26
27int main(){
28 Render(Tree{});
29 Render(Rock{});
30}
31
main.cpp(29,3): 'Render': no matching overloaded function found
could be 'void Render(T)'
the associated constraints are not satisfied
the concept 'Renderable<Rock>' evaluated to false
the concept 'std::same_as<int,bool>' evaluated to false
'int' and 'bool' are different types
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.