In this lesson, we’ll build on our previous knowledge of concepts to learn how to apply them to test more elaborate types, such as those defined by classes. By the end of this lesson, you will be able to:
requires
expressions==
and !=
using conceptsBy mastering the techniques covered in this lesson, you'll be able to write more robust and expressive generic code using class templates. You'll catch errors at compile-time, provide clearer intentions about template requirements, and create more maintainable and self-documenting code.
To check if a type has a specific member variable or function, we can use the scope resolution operator ::
within a requires
expression. Below, our Sized
concept is satisfied if the type has a member called Size
:
#include <iostream>
template <typename T>
concept Sized = requires { T::Size; };
struct Rock {
int Size;
};
int main() {
if constexpr (Sized<Rock>) {
std::cout << "Rock has a size";
}
}
Rock has a size
We can perform the same check within a functional-style requires
expression as follows:
template <typename T>
concept Sized = requires(T x) {
T::Size;
// Alternatively:
x.Size;
};
Remember, we can have multiple statements inside a requires
expression, and we can combine expressions using boolean logic. Below, we require our type either have a Size
member, or both a Width
and Height
 member:
template <typename T>
concept Sized = requires {
T::Size;
} || requires {
T::Width;
T::Height;
};
When we’re writing a concept that requires a type to have a specific member, we’ll typically also care about the exact nature of that member. In the following example, we check whether the Size
member satisfies the std::integral
 concept:
#include <concepts>
#include <iostream>
template <typename T>
concept IntegerSized =
std::integral<decltype(T::Size)>;
Within a requires
expression: our check could look like this:
#include <concepts>
template <typename T>
concept IntegerSized = requires {
requires std::integral<decltype(T::Size)>;
};
When we want to check if the member matches an exact type, we can use the std::same_as
concept. Below, we require our Size
member to be exactly an int
:
template <typename T>
concept IntegerSized =
std::same_as<int, decltype(T::Size)>;
However, an exact check like this is often more rigid than is necessary. For example, in most scenarios where our template requires an int
, types like an int&
or const int&
would also be acceptable.
To handle this, we can perform our comparison with reference and / or const
qualifiers removed from the type, using the std::remove_reference
and std::remove_const
type traits respectively.
We can remove both in a single step using the std::remove_cvref
type trait:
#include <concepts>
template <typename T>
concept IntegerSized =
std::same_as<int, std::remove_cvref_t<
decltype(T::Size)>>;
We covered this scenario, and type traits like std::remove_cvref
in more detail earlier in the chapter:
Typically, the friendliest way to require our types to have specific methods is to use a functional requires
expression, and then provide statements that use those methods.
In the following example, we require our type to have a Render()
method that is invocable with no arguments:
#include <iostream>
template <typename T>
concept Renderable = requires(T Object) {
Object.Render();
};
struct Rock {
void Render(){};
};
int main() {
if constexpr (Renderable<Rock>) {
std::cout << "Rock is renderable with no"
" arguments";
}
}
Rock is renderable with no arguments
std::invocable
In C++, functions also have a type, and like any other type, we can create pointers to them using the address-of operator &
.
In the following example, our Call()
function receives a function pointer as a parameter, and then invokes the function being pointed at:
#include <iostream>
template <typename T>
void Call(T Function) {
Function();
}
void Greet() {
std::cout << "Hello";
}
int main() {
Call(&Greet);
}
Hello
We cover techniques like this in a full chapter later in the course, but it’s worth covering how concepts can help here.
When our template expects a typename to be something that can be invoked, like a function pointer, we can constrain it using the std::invocable
 concept:
#include <iostream>
#include <concepts>
template <std::invocable T>
void Call(T Function) {
Function();
}
void Greet() {
std::cout << "Hello";
}
int main() {
Call(&Greet);
}
Hello
As with all concepts, we can use std::invocable
as a standalone expression to generate a boolean at compile time. We provide the type we’re testing as the first template argument.
In the following example, we use decltype
to provide the type of &Greet
:
#include <iostream>
#include <concepts>
void Greet() {
std::cout << "Hello";
}
int main() {
if (std::invocable<decltype(&Greet)>) {
std::cout << "Greet is invocable";
}
}
Greet is invocable
When the function we’re testing is a member of a class, we need to qualify its exact location using the scope resolution operator, as in SomeType::SomeMethod
.
We also have to provide the concept with a second template argument, which will be a pointer to our type. We discuss this second argument in more detail later in this lesson.
Putting these two properties together, our previous Renderable
concept could be written using std::invocable
like this:
#include <iostream>
template <typename T>
concept Renderable =
std::invocable<decltype(&T::Render), T*>;
struct Rock {
void Render(){};
};
int main() {
if constexpr (Renderable<Rock>) {
std::cout << "Rock is renderable with no"
" arguments";
}
}
Rock is renderable with no arguments
To require our methods to be callable with specific argument types, we can simply pass example values within the statement. Below, we require the Render()
method to be invocable with two int
 arguments.
The fact we’ve chosen 1
and 2
as the values is inconsequential - we can just use any values of the type we’re testing:
#include <concepts>
#include <iostream>
template <typename T>
concept Renderable = requires(T Object) {
Object.Render(1, 2);
};
struct Rock {
void Render(int x, int y){};
};
int main() {
if constexpr (Renderable<Rock>) {
std::cout << "Rock is renderable with "
"two integer arguments";
}
}
Rock is renderable with two integer arguments
We can alternatively add these as hypothetical arguments within the parameter list of our requires
syntax. This lets us explicitly specify the type, and also give the variables a name, which can be help document our assumptions:
template <typename T>
concept Renderable =
requires(T Object, int Width, int Height) {
Object.Render(Width, Height);
};
std::declval()
Within concepts, we can continue to use std::declval
to create hypothetical values, if preferred. Our previous Renderable
concept could be written like this instead:
template <typename T>
concept Renderable = requires(T Object) {
Object.Render(
std::declval<int>(), std::declval<int>());
};
Remember, our concepts are not restricted to a single template argument. Below, we update our Renderable
concept to accept three arguments.
Two of them - R1
and R2
are used when testing the parameter list of the Render()
 method:
#include <concepts>
#include <iostream>
template <typename T, typename R1, typename R2>
concept Renderable =
requires(T Object, R1 x, R2 y) {
Object.Render(x, y);
};
struct Rock {
void Render(int x, float y){};
};
int main() {
if constexpr (Renderable<Rock, int, float>) {
std::cout << "Rock is renderable with "
"an int and a float";
}
}
Rock is renderable with an int and a float
std::invocable
The std::invocable
concept accepts additional template arguments, beyond the type we’re testing. These template arguments represent the types we want our function to be invocable with.
Below, we check whether &Greet
is invocable with a std::string
and an int
:
#include <concepts>
#include <iostream>
void Greet(std::string Greeting, int Num) {
std::cout << Greeting << ", Num: " << Num;
}
int main() {
if constexpr (std::invocable<
decltype(&Greet), std::string, int>) {
std::cout << "Greet is invocable with a "
"std::string and an int";
}
}
Greet is invocable with a std::string and an int
Below, we’re using this concept to constrain a template type parameter.
Remember, when used in this context, the first argument to our concept is provided automatically during substitution, so we only provide the types our function will be invoked with:
#include <iostream>
#include <concepts>
template <std::invocable<std::string, int> T>
void Call(T Function) {
Function("Hello", 42);
}
void Greet(std::string Greeting, int Num) {
std::cout << Greeting << ", Num: " << Num;
}
int main() {
Call(&Greet);
}
Hello, Num: 42
Reviewing our earlier Renderable
concept, we can see it used std::invocable
like this, indicating that we’ll be calling T::Render
with a pointer to a T
:
template <typename T>
concept Renderable =
std::invocable<decltype(&T::Render), T*>;
This is typical with class methods. We can imagine the methods have a hidden argument, which will be a pointer to the object it was called on. For example, if we call SomeRock.Render()
, the hidden parameter will be &SomeRock
.
That is, it will be the value that is available through the this
pointer within our method body.
In the following example, we update our Renderable
concept to use std::invocable
with additional parameter requirements:
#include <iostream>
#include <concepts>
template <typename T, typename R1, typename R2>
concept Renderable = std::invocable<
decltype(&T::Render), T*, R1, R2>;
struct Rock {
void Render(int x, float y){};
};
int main() {
if constexpr (Renderable<Rock, int, float>) {
std::cout << "Rock is renderable with an"
" int and a float";
}
}
Rock is renderable with an int and a float
The syntax to test a method’s return value within a concept is a little more complex. In the following example, we’re requiring our object to have a Render()
method that accepts no arguments and returns something that satisfies the std::integral
 concept.:
template <typename T>
concept Renderable = requires(T Object) {
{ Object.Render() } -> std::integral;
};
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>;
};
Below, we are requiring the Render()
method to accept an argument of template parameter type A
and return something convertible to another template parameter type, R
:
#include <concepts>
#include <iostream>
template <typename T, typename A, typename R>
concept RenderReturns = requires(T Obj, A x) {
{ Obj.Render(x) } -> std::convertible_to<R>;
};
struct Rock {
int Render(int) { return 42; };
};
int main() {
if constexpr (RenderReturns<Rock, int, float>) {
std::cout << "Rock::Render(int) return type"
" is convertible to a float";
}
}
Rock::Render(int) return type is convertible to a float
As operators are essentially functions, we can test that a type has an operator in exactly the same way we’d test it have any other function.
Below, our concept requires that the type implement the ==
operator with another object of the same type being the right operand:
template <typename T>
concept Comparable = requires(T x, T y) {
x == y;
};
Below, we extend this to require the operator to return something that is convertible to a boolean:
#include <concepts>
#include <iostream>
struct Container {
int Value;
bool operator==(const Container& Other) const {
return Other.Value != Value;
}
};
template <typename T>
concept Comparable = requires(T x, T y) {
{ x == y } -> std::convertible_to<bool>;
};
int main() {
if constexpr (Comparable<Container>) {
std::cout << "Container is comparable";
}
}
Container is comparable
In this example, we expand the concept with an additional argument, allowing us to specify what type we’re going to be comparing our object to:
#include <concepts>
#include <iostream>
struct Container {/*...*/};
template <typename T1, typename T2>
concept ComparableTo = requires(T1 x, T2 y) {
{ x == y } -> std::convertible_to<bool>;
{ y == x } -> std::convertible_to<bool>;
{ x != y } -> std::convertible_to<bool>;
{ y != x } -> std::convertible_to<bool>;
};
int main() {
if constexpr (ComparableTo<Container, int>) {
std::cout << "Container is comparable to int";
}
}
Note, similar concepts to what we made above are already available in the standard library:
std::equality_comparable<Container>;
std::equality_comparable_to<Container, int>
In this lesson, we learned how to use concepts to test for more elaborate requirements. The key takeaways include:
::
within requires
expressions.std::integral
, std::floating_point
, std::same_as
, and type traits like std::is_base_of
and std::is_integral
can be used to constrain the types of class members.requires
expression with example parameter values, allowing you to specify parameter and return types.std::invocable
and related concepts.==
and !=
can be tested as normal functions within requires
expressions.&&
and ||
to define comprehensive constraints on class templates.Â
Learn how to use concepts to express constraints on classes, ensuring they have particular members, methods, and operators.
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.