requires
statements.In the previous lesson, we introduced C++20 concepts, which allow us to determine if a type meets some specific requirements at compile time. We used concepts like std::integral
and std::floating_point
in our examples, but of course, we’re not restricted to just what’s available in the standard library.
We can create our own concepts to test types against the exact requirements we have for our program. In this lesson, we’ll cover the key techniques to set this up.
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 we covered in the previous lesson:
#include <concepts>
#include <iostream>
template <typename T>
concept Integer = std::integral<T>;
// As a constrained template parameter
template <Integer T>
void ConstrainedTemplate(T x) {
std::cout << "That's an integer\n";
}
// As a compile-time boolean expression
template <typename T>
void UnconstrainedTemplate(T x) {
if constexpr (Integer<T>) {
std::cout << "That's an integer too\n";
}
}
int main() {
ConstrainedTemplate(1);
UnconstrainedTemplate(2);
}
That's an integer
That's an integer too
Our Integer
concept is effectively just re-implementing the std::is_integral
concept under a different name, but we can of course go further.
The following Numeric
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>;
The logic that ultimately returns the boolean representing whether or not the type satisfies the concept can be arbitrarily complex.
Additionally, C++20 introduced additional syntax involving the requires
keyword, which can make implementing this logic easier.
requires
ExpressionsWhen our requirements get more complex, trying to craft a single boolean expression to test for them can get messy. Instead, we can combine multiple requires
statements within a requires
expression. It looks like this:
template <typename T>
concept MyConcept = requires {
// ...
};
Between the braces, we list all of our requirements as individual statements. For the concept to be satisfied, all of the requirements must be satisfied. Below, we use this to create a concept that is satisfied if a type is both convertible to and convertible from an int
:
#include <concepts>
#include <iostream>
template <typename T>
concept ConvertibleToFromInt = requires {
requires std::convertible_to<int, T>;
requires std::convertible_to<T, int>;
};
template <ConvertibleToFromInt T>
void Template(int x) {
std::cout << "That was convertible: "
<< int(T{x});
}
template <typename T>
void Template(int x) {
std::cout << "\nThat was not";
}
struct SomeType {
SomeType(int x) : Value{x} {};
operator int() { return Value; };
int Value;
};
int main() {
Template<SomeType>(42);
Template<std::string>(42);
}
That was convertible: 42
That was not
For a type to satisfy a concept, the statements within the requires
expression must be valid for that type. That does not mean the expression needs to evaluate to true.
In the following example, we’re trying to write a requirement where a type must be integral. This concept will always be satisfied because, for any type, std::integral<T>
will be valid. It will not be a substitution failure - it will simply either be true
or false
:
#include <concepts>
#include <iostream>
template <typename T>
concept Integer = requires {
std::integral<T>;
};
int main() {
if (Integer<std::string>) {
std::cout << "The concept is satisfied";
}
}
The concept is satisfied
To assert that an expression must not only be valid but also true
, we should add the requires
keyword before it:
#include <concepts>
#include <iostream>
template <typename T>
concept Integer = requires {
requires std::integral<T>;
};
int main() {
if (!Integer<std::string>) {
std::cout << "The concept is not satisfied";
}
}
The concept is not satisfied
This distinction will make more sense as we see more examples later in the lesson.
requires
ExpressionsOur concepts can combine boolean expressions and requires
statements as needed to implement more complex checks. We do this using standard boolean operators like &&
and ||
.
Below, our concept requires the type to be a class or struct using the std::is_class
type trait, and additionally requires it be convertible to and from an int
using the same requires
expression we created earlier:
#include <concepts>
#include <iostream>
template <typename T>
concept IntClass = std::is_class_v<T> &&
requires {
requires std::convertible_to<int, T>;
requires std::convertible_to<T, int>;
};
struct SomeType {/*...*/};
int main() {
if (IntClass<SomeType>) {
std::cout << "The concept is satisfied";
}
}
The concept is satisfied
Below, our type must be a class or struct, and convertible to either a float
or an int
:
#include <concepts>
#include <iostream>
template <typename T>
concept IntOrFloatClass = std::is_class_v<T> &&
requires {
requires std::convertible_to<int, T>;
requires std::convertible_to<T, int>;
} || requires {
requires std::convertible_to<float, T>;
requires std::convertible_to<T, float>;
};
struct SomeType {/*...*/};
int main() {
if (IntOrFloatClass<SomeType>) {
std::cout << "The concept is satisfied";
}
}
The concept is satisfied
We can also negate requires
statements by negating the boolean they’re checking using the !
operator in the normal way. Below, we’re requiring our type be convertible from an int
, but not to an int
:
template <typename T>
concept SomeConcept = requires {
requires std::convertible_to<int, T>;
requires !std::convertible_to<T, int>;
};
We can also negate an entire requires
expression, which effectively translates to requiring at least one of the statements within the expression to be invalid.
Below, we reject a type that is simultaneously convertible from and to an int
. Being convertible in one direction (or neither) is fine, but not both:
template <typename T>
concept SomeConcept = !requires {
requires std::convertible_to<int, T>;
requires std::convertible_to<T, int>;
};
requires
SyntaxWe can also use a requires
expression that looks somewhat like a function:
template <typename T>
concept MyConcept = requires(T x) {
// ...
};
Within the body of the previous expression, we can imagine x
being a value of the type we’re testing - T
, in this case. We then provide statements that use this value:
template <typename T>
concept Addable = requires(T x) {
x + x;
};
It might look like we’re writing a function here, but what we’re doing is specifying requirements on the type T
. In the above example, we’re asking the compiler to imagine x
is an instance of our template type, T
.
We’re then asking it to determine whether x + x
would be a valid statement in that scenario. If this would be valid, then T
satisfies our concept.
In the following example, we’re providing 4 statements, all of which must be valid for T
to satisfy our Arithmetic
concept:
#include <iostream>
template <typename T>
concept Arithmetic = requires(T x) {
x + x;
x - x;
x * x;
x / x;
};
int main() {
if (Arithmetic<int>) {
std::cout << "int satisfies the concept";
}
if (!Arithmetic<std::string>) {
std::cout << "\nstd::string does not";
}
}
int satisfies the concept
std::string does not
Here is a final example, where we’re again saying T
must implement the +
operator where the right operand is to another T
.
But we have an additional requirement: whatever type is returned by that operator+
function must implement the /
operator, where the right operand is an int
such as 2
:
template <typename T>
concept Averagable = requires(T x, T y){
(x + y) / 2;
};
If a type meets those requirements, it is Averagable
, otherwise, it is not.
#include <string>
template <typename T>
concept Averagable =
requires(T x, T y){
(x + y) / 2;
};
int main(){
// This is fine
Average(3, 5);
// This is not
std::string A{"Hello"};
std::string B{"World"};
Average(A, B);
}
Before the introduction of concepts in C++20, 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:
error: 'Average': no matching overloaded function found
could be 'T Average(T,T)'
but the associated constraints are not satisfied
the concept 'Averagable<std::string>' evaluated to false
binary '/': 'std::string' does not define this operator
The compiler tells us that 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 also explains exactly why the concept returned false
- because the std::string
type does not define the /
operator we specified as a requirement.
Our previous concepts have using a single template parameter, but we are free to use multiple:
#include <iostream>
template <typename T1, typename T2>
concept Addable = requires(T1 x, T2 y) {
x + y;
};
int main() {
if (Addable<int, float>) {
std::cout << "int is addable to float";
}
if (!Addable<int, std::string>) {
std::cout << "\nbut not to std::string";
}
}
int is addable to float
but not to std::string
The order of our template parameters is important for the usual reason - it corresponds with the order of arguments.
However, when working with concepts, there is an additional consideration for our design. Specifically, the first argument will be provided automatically when the concept is used to constrain a template.
We’ve already seen examples of this. When using a concept like std::integral
as a boolean expression, we need to explicitly provide the type we’re testing within the <>
:
std::integral<int>
But, when using it to constrain a template, the argument is populated automatically during substitution:
#include <concepts>
#include <iostream>
template <std::integral T>
void Template(T x) {
std::cout << "\nThat was integral too";
}
int main() {
// We invoke the concept with int
if (std::integral<int>) {
std::cout << "That was integral";
}
// The compiler invokes the concept with int
Template(42);
}
That was integral
That was integral too
When the concept has multiple parameters, substitution provides the value for the first parameter.
This means that if we’re using a concept to constrain template parameters, and that concept requires multiple arguments, we need to provide the additional arguments within the argument list of our template.
In the following example, our concept is called with <int, float>
in both cases:
#include <concepts>
#include <iostream>
template <typename T1, typename T2>
concept AddableTo = requires(T1 x, T2 y) {
x + y;
};
template <AddableTo<float> T>
void Template(T x) {
std::cout << "that type is addable "
"to float too";
}
int main() {
if (AddableTo<int, float>) {
std::cout << "int is addable to float\n";
}
Template(42);
}
int is addable to float
that type is addable to float too
In this lesson, we covered how to create custom concepts in C++20 to define requirements for types. The key takeaways are:
concept
keyword and a boolean expression or a requires
expression.requires
expression allows us to list multiple requirements that must all be satisfied for the concept to be met.requires
expressions using boolean operators like &&
, ||
, and !
.requires
syntax to specify requirements on how instances of a type can be used.Learn how to create your own C++20 concepts to define precise requirements for types, using boolean expressions and requires
statements.
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.