C++20 Concepts & Constraints
Define precise requirements on template arguments using concepts. Write cleaner generic code with requires clauses, abbreviated function templates, and standard library concepts.
The Problem Concepts Solve
Before concepts, template error messages were cryptic — using std::sort on a type without operator< could produce pages of error text deep inside the standard library implementation. Concepts solve this by letting you state requirements up front on template parameters.
A concept is a named boolean predicate evaluated at compile time. It checks whether a type satisfies a set of syntactic and semantic requirements. When a concept is not satisfied, the compiler produces a clear, concise error message pointing to the unsatisfied requirement — not a wall of template instantiation backtraces.
Concepts were introduced in C++20 and represent the most significant improvement to generic programming since templates themselves.
Defining Concepts
A concept is defined with the concept keyword followed by a requires expression that specifies what operations a type must support. The requires expression can check for valid expressions, return types, nested types, and more.
#include <concepts>
#include <iostream>
#include <string>
// Define a concept: T must support +, ==, and copy construction
template <typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>; // a+b must return something convertible to T
{ a == b } -> std::convertible_to<bool>; // must be comparable
requires std::copy_constructible<T>; // nested requirement
};
// Concept for types that have .size() and .empty()
template <typename T>
concept Sizable = requires(const T& t) {
{ t.size() } -> std::convertible_to<std::size_t>;
{ t.empty() } -> std::convertible_to<bool>;
};
// Use the concept to constrain a template
template <Addable T>
T sum_three(T a, T b, T c) {
return a + b + c;
}
template <Sizable Container>
void report_size(const Container& c) {
std::cout << "Size: " << c.size()
<< (c.empty() ? " (empty)" : "") << '\n';
}
int main() {
std::cout << sum_three(1, 2, 3) << '\n'; // 6
std::cout << sum_three(1.5, 2.5, 3.0) << '\n'; // 7.0
std::string s = "hello";
report_size(s); // Size: 5
std::vector<int> v{1, 2, 3};
report_size(v); // Size: 3
}Requires Clauses & Abbreviated Syntax
There are multiple ways to apply a concept to a template. The requires clause goes after the template parameter list or after the function signature. The abbreviated function template syntax uses Concept auto as a parameter type, which is the most concise form.
#include <concepts>
#include <iostream>
// Method 1: Concept as template parameter constraint
template <std::integral T>
T gcd(T a, T b) {
while (b != 0) {
T temp = b;
b = a % b;
a = temp;
}
return a;
}
// Method 2: requires clause after template parameters
template <typename T>
requires std::integral<T>
T lcm(T a, T b) {
return (a / gcd(a, b)) * b;
}
// Method 3: Trailing requires clause
template <typename T>
T absolute(T value) requires std::signed_integral<T> {
return value < 0 ? -value : value;
}
// Method 4: Abbreviated function template (Concept auto)
void print_integral(std::integral auto value) {
std::cout << "Integral: " << value << '\n';
}
// Concept auto works for return types too (C++20)
std::integral auto compute() {
return 42; // must return an integral type
}
int main() {
std::cout << gcd(12, 8) << '\n'; // 4
std::cout << lcm(12, 8) << '\n'; // 24
std::cout << absolute(-42) << '\n'; // 42
print_integral(100); // Integral: 100
// print_integral(3.14); // ERROR: constraint not satisfied
}Concept Composition & Standard Concepts
Concepts compose naturally with && (conjunction) and || (disjunction). The standard library provides a rich set of concepts in :
Core language concepts:
- std::same_as — T and U are the same type
- std::derived_from — Derived is derived from Base
- std::convertible_to — From is implicitly convertible to To
- std::integral, std::floating_point, std::signed_integral
Comparison concepts:
- std::equality_comparable — supports == and !=
- std::totally_ordered — supports <, >, <=, >=, ==, !=
Object concepts:
- std::movable, std::copyable, std::semiregular, std::regular
Callable concepts:
- std::invocable — F can be called with Args
- std::predicate — F is a predicate returning bool
Iterator concepts ():
- std::input_iterator, std::forward_iterator, std::random_access_iterator, std::contiguous_iterator
Range concepts ():
- std::ranges::range, std::ranges::sized_range, std::ranges::random_access_range
Concept Subsumption & Overload Resolution
When multiple constrained overloads match, the compiler uses concept subsumption to pick the most specific one. Concept A subsumes concept B if A's constraints logically imply B's constraints. This allows "refinement" — a more specific concept wins over a more general one.
#include <concepts>
#include <iostream>
// Concept hierarchy: sorted_range is "more specific" than range
template <typename T>
concept Printable = requires(std::ostream& os, const T& t) {
{ os << t };
};
template <typename T>
concept PrintableNumber = Printable<T> && std::integral<T>;
// Less constrained overload
template <Printable T>
void display(const T& value) {
std::cout << "[generic] " << value << '\n';
}
// More constrained overload — subsumes Printable
template <PrintableNumber T>
void display(const T& value) {
std::cout << "[number] " << value << '\n';
}
int main() {
display(42); // [number] 42 — PrintableNumber is more specific
display(std::string("hi")); // [generic] hi — only Printable matches
display(3.14); // [generic] 3.14 — not integral
}The requires requires Pattern
The somewhat unusual requires requires appears when you use an ad-hoc requires expression directly in a requires clause without defining a named concept. The first requires introduces the constraint clause; the second requires begins the requires expression.
#include <iostream>
#include <string>
// Ad-hoc constraint without naming a concept
template <typename T>
requires requires(T a, T b) {
{ a + b } -> std::same_as<T>;
}
T add(T a, T b) {
return a + b;
}
// Equivalent named concept (preferred for readability)
template <typename T>
concept AddableToSelf = requires(T a, T b) {
{ a + b } -> std::same_as<T>;
};
template <AddableToSelf T>
T add_named(T a, T b) {
return a + b;
}
int main() {
std::cout << add(1, 2) << '\n'; // 3
std::cout << add(std::string("a"), std::string("b")) << '\n'; // ab
std::cout << add_named(10, 20) << '\n'; // 30
}When designing concepts:
- Name concepts after what the type IS, not what the function needs — use Sortable not HasLessThan. Concepts should capture semantic categories.
- Prefer standard library concepts — before writing your own, check if , , or already has what you need.
- Keep concepts small and composable — combine small concepts with && rather than creating monolithic requirements.
- Use named concepts over ad-hoc requires requires — the named form is more readable and reusable.
- Document semantic requirements in comments — concepts check syntax, not semantics. If you require operator+ to be commutative, say so in a comment.
- Concepts are named boolean predicates that constrain template parameters, providing clear error messages
- The
requiresexpression checks for valid expressions, return types, and nested requirements - Four ways to apply concepts: constraint on template param, requires clause, trailing requires, or
Concept auto - Concepts compose with
&&and||; subsumption selects the most constrained overload - The standard library provides concepts in
,, and requires requiresis an ad-hoc inline constraint — prefer named concepts for readability
Quiz — Test Your Knowledge
(20 XP)1. What does `void print(std::integral auto value)` mean?
2. If concept A is defined as `concept A = B<T> && C<T>`, what happens when both a B-constrained and an A-constrained overload match?
3. Why does `requires requires(T a) { a.size(); }` have the keyword 'requires' twice?