Concepts
Concepts (C++20) state what a template needs from its type arguments: "a number", "something sortable", "a type with a size() member". They make template code self-documenting, let overloads choose by capability, and turn pages of cryptic template errors into one clear message.
Using standard concepts
The <concepts> header provides many ready-made concepts. Put one in place of typename to constrain a parameter:
#include <concepts> #include <iostream> template <std::integral T> T gcd(T a, T b) { while (b != 0) { T r = a % b; a = b; b = r; } return a; } int main() { std::cout << gcd(1071, 462) << ' ' << gcd(48L, 18L) << '\n'; }
21 6
Calling gcd(2.5, 1.5) is now rejected immediately, with a message that names the unmet requirement:
#include <concepts> template <std::integral T> T gcd(T a, T b) { return b == 0 ? a : gcd(b, a % b); } int main() { return static_cast<int>(gcd(2.5, 1.5)); }
main.cpp:7:29: error: no matching function for call to 'gcd'
| Concept | Satisfied by |
|---|---|
std::integral, std::floating_point | Integer / floating-point types |
std::same_as<T, U>, std::convertible_to<T, U> | Type relationships |
std::equality_comparable, std::totally_ordered | Types with == / with < and friends |
std::invocable<F, Args...>, std::predicate | Callables |
std::ranges::range, std::ranges::random_access_range | Things you can iterate |
Three ways to write constraints
#include <concepts> #include <iostream> template <std::floating_point T> // 1. in the template header T half(T x) { return x / 2; } template <typename T> requires std::integral<T> // 2. a requires-clause T twice(T x) { return x * 2; } auto negate(std::signed_integral auto x) { // 3. constrained auto parameter return -x; } int main() { std::cout << half(5.0) << ' ' << twice(21) << ' ' << negate(7) << '\n'; }
2.5 42 -7
Defining your own concepts
A requires-expression lists operations that must compile for the type:
#include <concepts> #include <iostream> #include <string> #include <vector> template <typename T> concept HasArea = requires(const T& shape) { { shape.area() } -> std::convertible_to<double>; }; struct Square { double side; double area() const { return side * side; } }; struct Circle { double r; double area() const { return 3.14159 * r * r; } }; template <HasArea... Shapes> double total_area(const Shapes&... shapes) { return (shapes.area() + ...); // a fold expression over all arguments } int main() { std::cout << total_area(Square{2}, Circle{1}, Square{1}) << '\n'; }
8.14159
Unlike the abstract classes of lesson 38, Square and Circle don't inherit from anything: any type with a suitable area() qualifies, and there are no virtual calls. (Shapes... is a parameter pack — a template taking any number of arguments.)
Overloading on concepts
When several constrained overloads match, the compiler picks the most specific one — a clean replacement for older tricks like enable_if:
#include <concepts> #include <iostream> #include <string> template <typename T> concept Numeric = std::integral<T> || std::floating_point<T>; void describe(Numeric auto x) { std::cout << x << " is a number\n"; } void describe(std::integral auto x) { std::cout << x << " is a whole number\n"; } void describe(const std::string& s) { std::cout << '"' << s << "\" is text\n"; } int main() { describe(3.5); describe(42); // both match; std::integral is more specific describe(std::string{"hi"}); }
3.5 is a number 42 is a whole number "hi" is text
Clear error messages
Here's what happens when a type doesn't meet a custom concept — the compiler names the concept and the failing requirement:
#include <concepts> template <typename T> concept HasArea = requires(const T& s) { { s.area() } -> std::convertible_to<double>; }; double report(const HasArea auto& shape) { return shape.area(); } struct Line { double length; }; int main() { return static_cast<int>(report(Line{3.0})); }
main.cpp:11:29: error: no matching function for call to 'report'
Try it yourself
Write a concept Container that requires size() and begin()/end(), and a function that prints the size of anything satisfying it.
container with 3 elements container with 5 elements container with 2 elements