Concepts (C++20)
2 min read
Concepts (C++20)
Concepts constrain template parameters, providing clear compile-time errors and enabling function overloading on type properties. Before concepts, a template that received the wrong type produced a wall of cryptic error messages deep inside the template instantiation. With concepts, the error points directly to the call site.
The problem without concepts
template <typename T>
T max_of(T a, T b) { return (a > b) ? a : b; }
max_of("hello", "world"); // works but compares pointers, not content
The template accepts anything with > — including types where the behavior is wrong.
Defining a concept
A concept is a named boolean predicate on types:
#include <concepts>
#include <iostream>
template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
template <Numeric T>
T max_of(T a, T b) { return (a > b) ? a : b; }
int main() {
std::cout << max_of(3, 7) << "\n"; // OK
std::cout << max_of(1.5, 2.5) << "\n"; // OK
// max_of("a", "b"); // compile error: const char* does not satisfy Numeric
}
Standard library concepts
<concepts> provides a large set of standard concepts:
| Concept | Meaning |
|---|---|
std::integral<T> |
T is an integer type |
std::floating_point<T> |
T is a floating-point type |
std::same_as<T, U> |
T and U are the same type |
std::derived_from<T, B> |
T is derived from B |
std::convertible_to<T, U> |
T is convertible to U |
std::equality_comparable<T> |
T supports == and != |
std::totally_ordered<T> |
T supports all six comparisons |
std::copy_constructible<T> |
T can be copy-constructed |
std::invocable<F, Args...> |
F can be called with Args |
requires expressions
For custom constraints, requires describes what operations a type must support:
template <typename T>
concept Printable = requires(T x) {
{ std::cout << x }; // must support stream insertion
};
template <Printable T>
void print(T value) {
std::cout << value << "\n";
}
print(42); // OK
print("hello"); // OK
// print(std::mutex{}); // error: std::mutex is not Printable
Concept-based overloading
Concepts enable overloading based on type properties:
#include <concepts>
#include <iostream>
template <std::integral T>
void describe(T x) {
std::cout << x << " is an integer\n";
}
template <std::floating_point T>
void describe(T x) {
std::cout << x << " is a float\n";
}
int main() {
describe(42); // 42 is an integer
describe(3.14); // 3.14 is a float
}
requires clause vs. concept syntax
Multiple ways to express the same constraint:
// 1. Named concept in template parameter
template <std::integral T>
T halve(T x) { return x / 2; }
// 2. requires clause after parameters
template <typename T>
requires std::integral<T>
T halve(T x) { return x / 2; }
// 3. constrained auto (abbreviated template)
auto halve(std::integral auto x) { return x / 2; }
All three are equivalent. Abbreviated template syntax (option 3) is the most concise for simple cases.
Learn modern C++ through working code. Each chapter introduces one concept — variables, functions, classes, templates, smart pointers, concurrency — with a clear example, a line-by-line explanation, and notes on how it applies in real programs.
View book →