Given that SFINAE is excessively complex and inelegant, C++20 introduced a new language feature that almost entirely replaces it: concepts
Concepts give us a more direct and powerful way of specifying constraints on template parameters. This feature allows us to write constraints in a much easier and more readable way. 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.
In the next lesson, we’ll show how we can create our own concepts, specifying the exact requirements we need for our specific use cases.
We can imagine a concept being a set of requirements for a type. We pass the type to a concept at compile time, and the concept returns true
if the type meets the requirements. Requirements can include things like:
Player
?Render()
that accepts an int
and returns void
?The standard library includes a collection of pre-defined concepts that cover the first three examples. In the next lesson, we’ll see how we can create our own concepts for our highly specific requirements.
Most of the standard library concepts are available from the <concepts>
 header:
#include <concepts>
For example, the std::integral
concept can tell us if a type is an integer: We pass the type as a template argument, and get a boolean back 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
Of course, we could have done this exact same thing using type traits. What makes concepts different from type traits is that the C++ specification allows concepts to interact with templates as a feature of the language itself.
This capability is baked into the syntax of the language, and there are four main ways we can use it.
Within a template parameter list, we can replace typename
types with the name of a concept. This parameter will still receive a type name as an argument. But now, for the template to be a valid candidate, that type must implement the requirements specified in the concept.
So, if we wanted to ensure our type was an integer, we no longer need to manually create assertions or implement SFINAE. Instead, we can just replace typename
with the name of 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 our template will not appear within the overload candidate list when non-integral arguments are provided.
Additionally, the compiler will generate meaningful error messages explaining why our template wasn’t an option: In this case, it was because the double
arguments we provided were not integral:
#include <concepts>
template <std::integral T>
T Average(T x, T y) {
return (x + y) / 2;
}
int main() {
// This is fine
Average(1, 2);
// This is not
Average(1.5, 2.2);
}
error: 'Average': no matching overloaded function found
could be 'T Average(T,T)' but the associated constraints are not satisfied
the concept 'std::integral<double>' evaluated to false
Remember, just because a set of arguments did not satisfy a concept, that does not necessarily result in a compilation error. It just means that that specific template is not a viable overload for the function call.
Another function may be, and compilation can continue:
#include <iostream>
#include <concepts>
template <std::integral T>
T Average(T x, T y) {
std::cout << "Using template function\n";
return (x + y) / 2;
}
float Average(float x, float y) {
std::cout << "Using regular function\n";
return (x + y) / 2;
}
int main() {
Average(1, 2);
Average(1.5, 2.2);
}
Using template function
Using regular function
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
In our Overload Resolution lesson, we mentioned that the compiler prefers to choose templates that are constrained by concepts over those that aren’t.
Below, we have a general, unconstrained function template that can be instantiated with any typename
.
We also have a template function with the same name, constrained such that only type names that satisfy std::integral
are supported. When the arguments are integral, the compiler selects the more specialized template:
#include <concepts>
#include <iostream>
template <std::integral T>
T Average(T x, T y) {
std::cout << "Using integral function\n";
return (x + y) / 2;
}
template <typename T>
T Average(T x, T y) {
std::cout << "Using generic function";
return (x + y) / 2;
}
int main() {
Average(1, 2);
Average(1.5, 2.2);
}
Using integral function
Using generic function
This gives us similar behaviour to what we described in our section on template specialization. The key difference is that with template specialization, we need to specify an exact type.
With concepts, we can be much more flexible.
The examples in this lesson use function templates, but concepts can be used with other forms of templates too. The syntax is broadly the same. Below, we create a template class that uses a concept to ensure it can only be instantiated with an integral type argument:
#include <concepts>
template <std::integral T>
class Container {
T Contents;
};
int main() {
// This is fine
Container<int> IntegerThing;
// This is not
Container<float> FloatingThing;
}
'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() {
// This is fine
Average(1, 2);
Average(3.4, 5.3);
// This is not
Average("Hello", "World");
}
error: 'Average': no matching overloaded function found
the associated constraints are not satisfied
the concept 'std::integral<const char*>' evaluated to false
the concept 'std::floating_point<const char*>' evaluated to false
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 to state our template is only compatible with types that derive from Player
or Monster
:
#include <concepts>
class Player {};
class Monster {};
class Goblin : public Monster {};
class Rock {};
template <typename T>
requires std::is_base_of_v<Player, T> ||
std::is_base_of_v<Monster, T>
void Function(T Character) {}
int main() {
// This is fine
Function(Player{});
Function(Monster{});
Function(Goblin{});
// This is not
Function(Rock{});
}
error: 'Function': no matching overloaded function found
the associated constraints are not satisfied
Many of the standard library-type traits that are intended to determine if a type meets a requirement have variations that use concepts instead. Most type traits that return a boolean value
are in this category.
For example, the std::base_of
type trait we used in the previous example could be replaced with the std::derived_from
concept. We just need to reverse the order of the two arguments:
template <typename T>
requires std::derived_from<T, Player> ||
std::derived_from<T Monster>
void Function(T Character) {}
When we have a type trait that is intended to determine if a type meets some requirement, we should consider switching to a concept instead, as concepts are specifically designed for this use case.
requires
Keyword with Non-Function TemplatesWe can use a requires statement with other templates in the way we might expect. Below, we apply it to a class template:
#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 main use case for this is when we have a function that is not a template, but is a member of a class or struct that is.
This effectively allows us to disable member functions based on the template parameters of the class or struct they’re part of:
#include <concepts>
template <typename T>
struct Container {
void Function()
requires std::integral<T>
{}
};
int main() {
// This is fine
Container<int> IntContainer;
IntContainer.Function();
// This is not
Container<float> FloatContainer;
FloatContainer.Function();
}
error: 'Function': no function satisfied its constraints
the 'Container<float>::Function' constraint was not satisfied
the concept 'std::integral<float>' evaluated to false
The final way we can use concepts is within abbreviated function templates. As a reminder, abbreviated function templates let us create template functions simply by setting one or more parameter types to auto
:
auto Average(auto x, auto y) {
return (x + y) / 2;
}
We can constrain these parameters using concepts too. We insert the concept before the auto
 type:
#include <concepts>
auto Average(std::integral auto x,
std::integral auto y) {
return (x + y) / 2;
}
int main() {
// This is fine
Average(1, 2);
// This is not
Average(1.0, 2.0);
}
error: 'Average': no matching overloaded function found
the concept 'std::integral<double>' evaluated to false
the constraint was not satisfied
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. In this case, the first type must be integral, whilst the second must be either integral or floating point:
template <std::integral T1, typename T2>
requires std::integral<T2> ||
std::floating_point<T2>
void Function(T1x , T2 y){}
In this lesson, we explored the powerful new feature introduced in C++20: concepts. Concepts provide a more expressive and readable way to constrain template parameters and improve template error messages. The key takeaways from this lesson are:
std::integral
 and std::floating_point
.There are four main ways to use concepts with templates, and we can combine them as needed:
typename
 with the name of a concept to constrain the template parameter.requires
 Keyword: Use the requires
 keyword to specify constraints on template parameters using boolean logic and type traits.requires
 statement between the function signature and the function body to disable member functions based on the template parameters of the class or struct they're part of.auto
 parameters in abbreviated function templates by inserting the concept before the auto
 type.Learn how to use C++20 concepts to constrain template parameters, improve error messages, and enhance code readability.
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.