Concepts — Advanced Usage
Advanced C++20 concepts: requires expressions, subsumption, concept composition, abbreviated templates, and constraint-driven API design.
C++20 Concepts (Advanced)since C++20Concepts constrain template parameters through named, composable Boolean predicates that participate in overload resolution via subsumption, replacing SFINAE with precise, readable compile-time interfaces.
Overview
Concepts go beyond simple type checks. The machinery underneath — requires expressions, atomic constraints, and subsumption rules — enables disciplined overload sets, algebraic type hierarchies, and API contracts that survive refactoring. This page assumes familiarity with concept, requires, and the standard concepts in <concepts>, and focuses on the mechanics that matter in production code.
Syntax
Requires expressions — four requirement kinds
A requires expression evaluates to bool at compile time and never executes. Four kinds of requirements compose inside one body:
// C++20
template<typename T>
concept Iterator = requires(T it, T other) {
// 1. Simple — expression must be well-formed
*it;
++it;
// 2. Type — associated type must exist
typename T::value_type;
typename T::difference_type;
// 3. Compound — expression valid AND return type satisfies a concept
{ *it } -> std::same_as<typename T::reference>;
{ it - other } -> std::convertible_to<typename T::difference_type>;
// 4. Nested — an additional constraint that must hold
requires std::copyable<T>;
requires std::equality_comparable<T>;
};The compound form { expr } -> Concept checks that expr is valid and that its type satisfies Concept<decltype((expr))>. The parentheses in decltype((expr)) mean reference qualification is preserved: { *it } -> std::same_as<int&> requires an lvalue int, not a prvalue.
Requires clause vs. requires expression
// Requires clause — constrains a template; appears after template params or after signature
template<typename T>
requires std::integral<T> // clause: must be a Boolean constant expression
void f(T x);
// Requires expression — a Boolean expression you can use anywhere
constexpr bool b = requires(int x) { x + 1; }; // true
// A clause can contain an expression (ugly but legal):
template<typename T>
requires requires(T x) { x++; } // clause wrapping an expression
void g(T x);
// Always prefer a named concept here.Never use bare requires { ... } in a real function signature. Name the concept.
Examples
Subsumption and partial ordering of constraints
Subsumption is what makes concept-based overloading deterministic. The compiler decomposes each concept into atomic constraints — individual requires expressions or concept specialisations that cannot be further split. Overload A is more constrained than overload B when every atomic constraint in B's decomposition also appears in A's decomposition.
// C++20
template<typename T>
concept Addable = requires(T a, T b) { a + b; };
template<typename T>
concept Ring = Addable<T> && requires(T a, T b) { a * b; };
// ^^^^^^^^^ — reuses Addable's exact atoms
template<Addable T> void process(T x) { /* general */ }
template<Ring T> void process(T x) { /* optimised */ }
process(1); // int: satisfies both — Ring subsumes Addable, picks Ring overload
process(1.0); // double: sameThe critical requirement: Ring must name Addable in a conjunction, not restate equivalent logic. Two independent requires(T a, T b) { a + b; } blocks in two separate concepts are different atomic constraints; subsumption fails and the call becomes ambiguous:
// BROKEN — logically equivalent but atomically different
template<typename T>
concept HasPlus = requires(T a, T b) { a + b; };
template<typename T>
concept HasPlusAndTimes = requires(T a, T b) { a + b; a * b; }; // re-stated, not reused
template<HasPlus T> void h(T);
template<HasPlusAndTimes T> void h(T); // ERROR: ambiguous — subsumption doesn't fireTiered overloading via the standard iterator hierarchy
The standard library concepts are designed with subsumption in mind: std::random_access_iterator subsumes std::bidirectional_iterator which subsumes std::input_iterator. This makes tiered dispatch clean:
// C++20
#include <iterator>
#include <concepts>
// Tier 1: O(n), forward only
template<std::input_iterator It>
void my_advance(It& it, std::iter_difference_t<It> n) {
while (n-- > 0) ++it;
}
// Tier 2: O(n), bidirectional — also handles n < 0
template<std::bidirectional_iterator It>
void my_advance(It& it, std::iter_difference_t<It> n) {
if (n >= 0) while (n-- > 0) ++it;
else while (n++ < 0) --it;
}
// Tier 3: O(1), random access
template<std::random_access_iterator It>
void my_advance(It& it, std::iter_difference_t<It> n) {
it += n;
}
// std::list::iterator → Tier 2
// std::vector::iterator → Tier 3
// No tag dispatch, no if constexpr, no SFINAE.Constrained class template members
Non-template member functions can carry requires clauses in C++20, enabling opt-in operations that only compile for appropriate instantiations:
// C++20
template<typename T>
class Matrix {
public:
// Only instantiated when T is floating-point
Matrix operator*(const Matrix&) const
requires std::floating_point<T>;
// Only for integer element types
Matrix operator%(const Matrix&) const
requires std::integral<T>;
};
Matrix<double> md; md * md; // OK
Matrix<int> mi; mi % mi; // OK
// mi * mi; // error: constraint not satisfiedBuilding algebraic concept hierarchies
// C++20
namespace algebra {
template<typename T>
concept AdditiveGroup = requires(T a, T b) {
{ a + b } -> std::same_as<T>;
{ a - b } -> std::same_as<T>;
{ -a } -> std::same_as<T>;
T{}; // additive identity
};
template<typename T>
concept Ring = AdditiveGroup<T> && requires(T a, T b) {
{ a * b } -> std::same_as<T>;
T{1}; // multiplicative identity
};
template<typename T>
concept Field = Ring<T> && requires(T a, T b) {
{ a / b } -> std::same_as<T>;
};
template<Field T>
T lerp(T a, T b, T t) { return a + t * (b - a); }
} // namespace algebra
// double, float, std::complex<double> → Field
// int → Ring but not Field (no division closure)
// Verified at call site with a readable diagnostic.
static_assert(algebra::Field<double>);
static_assert(algebra::Ring<int>);
static_assert(!algebra::Field<int>);Ring subsumes AdditiveGroup because it names it directly. Adding a Ring-specific overload alongside an AdditiveGroup overload would resolve correctly.
Best Practices
Name every non-trivial constraint. A bare requires requires(T x) { ... } in a function signature is a maintenance trap and blocks subsumption.
Compose through inclusion, not repetition. For subsumption to fire, the more-constrained concept must name the less-constrained one in a conjunction (&&). Restating the same logic in two separate requires expressions gives you two different atomic constraints that the compiler treats as unrelated.
Use abbreviated templates for single-parameter leaf functions. void f(std::integral auto x) is clearest when the type name is not needed elsewhere in the signature. When two parameters must share a type, fall back to the named-parameter form: template<std::integral T> T add(T a, T b).
Test your concept boundaries with static assertions. Place them at the bottom of the header that defines the concept. They act as regression tests when types or concepts evolve.
Prefer std::convertible_to over std::same_as in compound requirements unless you truly need an exact type match including reference category.
Common Pitfalls
Negated constraints and overload sets
// C++20
template<typename T> requires std::integral<T> void f(T); // (1)
template<typename T> requires !std::integral<T> void f(T); // (2)
// This is well-formed and unambiguous because the constraints are disjoint.
// But negation does NOT produce a subsumption relationship:
// (1) is not "more constrained" than (2) or vice versa.
// Adding a third overload with no constraint would create ambiguity with (2)
// for non-integral types. Use if constexpr inside a single template
// when you need a true catch-all fallback.Compound constraints check the exact expression type
template<typename T>
concept NarrowIterator = requires(T it) {
{ *it } -> std::same_as<int>; // fails if *it is int& or const int&
};
template<typename T>
concept BroadIterator = requires(T it) {
{ *it } -> std::convertible_to<int>; // accepts int, int&, const int&, long, ...
};std::same_as in a compound requirement uses decltype((expr)), which retains reference and cv-qualifiers. Most iterator concepts use std::same_as<typename T::reference> — match the reference type exactly — but for ad-hoc utility concepts std::convertible_to is usually the right tool.
Identical logic in separate concepts breaks subsumption
This is the single most common subsumption mistake. If you find yourself writing equivalent requires bodies in two concepts without one naming the other, refactor: extract the shared requirement into a base concept and name it in both.
Concept Decomposition with requires-expressions
Named concepts keep signatures readable and enable subsumption. A requires expression can verify multiple properties at once; wrapping it in a named concept turns it into a reusable, composable predicate.
// C++20
#include <concepts>
#include <cstddef>
// Named concept wrapping a single requires expression
template<typename T>
concept StringLike = requires(T t) {
// Simple requirement — expression must be well-formed
t.size();
t.data();
// Compound requirements — well-formed AND return type satisfies Concept
{ t.size() } -> std::convertible_to<std::size_t>;
{ t.data() } -> std::same_as<const char*>;
// Type requirement — associated type must exist
typename T::value_type;
// Nested requirement — a further constraint that must hold
requires std::copyable<T>;
};The four requirement forms can be freely mixed inside one requires body. Simple requirements check expression validity only. Type requirements assert an associated type exists. Compound requirements ({ expr } -> Concept) check both validity and that the expression's type satisfies Concept. Nested requirements embed an additional Boolean constraint.
// C++20
// Combining multiple named concepts via conjunction
template<typename T>
concept BufferLike = StringLike<T> && requires(T t) {
// additional operations needed for buffer semantics
{ t.empty() } -> std::same_as<bool>;
{ t[0] } -> std::convertible_to<char>;
};
template<BufferLike T>
void transmit(const T& buf) {
// guaranteed: .data(), .size(), .empty(), operator[] all well-formed
}Prefer names over inline requires requires(...) in function signatures — naming improves diagnostics and allows subsumption to fire.
Recursive and Self-Referential Concepts
Concepts can depend on other concepts, forming dependency chains. This is not recursion in the traditional sense — concepts cannot refer to themselves — but a concept may constrain associated types using other concepts, creating a compositional hierarchy.
// C++20
#include <concepts>
#include <ostream>
#include <ranges>
template<typename T>
concept Printable = requires(T t, std::ostream& os) {
os << t; // stream-insertion must be well-formed
};
// PrintableContainer depends on both std::ranges::range and Printable
template<typename T>
concept PrintableContainer =
std::ranges::range<T> &&
Printable<std::ranges::range_value_t<T>>;The standard library follows this pattern throughout <ranges>. std::ranges::input_range requires that std::ranges::iterator_t<T> satisfies std::input_iterator. std::input_iterator in turn requires std::weakly_incrementable and std::indirectly_readable. Each layer builds on the previous one by naming it.
// C++20
// Use the composed concept: works for vector<int>, list<string>, etc.
template<PrintableContainer C>
void print_all(const C& c) {
for (const auto& item : c)
std::cout << item << '\n';
}
// static_assert shows the dependency in action
static_assert(PrintableContainer<std::vector<int>>);
static_assert(!PrintableContainer<std::vector<std::vector<int>>>);Each concept in the chain documents one semantic layer. Keeping layers small makes error messages point to the exact property that failed.
Concept-Based Overloading and Dispatch
The compiler selects the most-constrained overload that is satisfied. This is subsumption at work: when multiple overloads match, the one whose constraint set is a strict superset of another's wins; if no ordering exists, the call is ambiguous.
// C++20
#include <iterator>
// Tier 1 — any input iterator: O(n), forward only
template<std::input_iterator It>
void advance(It& it, std::iter_difference_t<It> n) {
while (n-- > 0) ++it;
}
// Tier 2 — bidirectional: O(n), supports negative n
// std::bidirectional_iterator subsumes std::input_iterator
template<std::bidirectional_iterator It>
void advance(It& it, std::iter_difference_t<It> n) {
if (n >= 0) while (n-- > 0) ++it;
else while (n++ < 0) --it;
}
// Tier 3 — random access: O(1)
// std::random_access_iterator subsumes std::bidirectional_iterator
template<std::random_access_iterator It>
void advance(It& it, std::iter_difference_t<It> n) {
it += n;
}The subsumption chain random_access_iterator ⊃ bidirectional_iterator ⊃ input_iterator is built into the standard library: each concept names the previous one in a conjunction. No tags, no SFINAE, no manual priority tricks.
// C++20
// Contrast: if constexpr dispatch — works but is not overloading.
// All three branches live in one function body; you cannot specialise it further.
template<typename It>
void advance_monolithic(It& it, std::iter_difference_t<It> n) {
if constexpr (std::random_access_iterator<It>) it += n;
else if constexpr (std::bidirectional_iterator<It>) {
if (n >= 0) while (n-- > 0) ++it;
else while (n++ < 0) --it;
} else {
while (n-- > 0) ++it;
}
}
// Prefer the overload set: it's open to extension without modifying existing code.Abbreviated Function Templates with Concepts
auto parameters in free functions are syntactic sugar for unnamed template parameters. C++20 extended the abbreviated template syntax from generic lambdas to ordinary functions: writing a concept name before auto applies the concept as a constraint on that implicit parameter.
// C++20
#include <concepts>
// These two declarations are exactly equivalent:
void process(std::integral auto x);
template<std::integral T>
void process(T x);Each auto introduces an independent template parameter. When two parameters share a constraint but need not share a type, abbreviated syntax is concise; when they must share the same type, use the explicit template form.
// C++20
// Each `auto` here is a separate, independent template parameter.
// a and b may be different integral types.
auto sum(std::integral auto a, std::integral auto b) {
return a + b;
}
// If a and b must share the same type, use the named form:
template<std::integral T>
T strict_sum(T a, T b) { return a + b; }
// Abbreviated form composes cleanly with trailing return types:
std::integral auto clamp(std::integral auto val,
std::integral auto lo,
std::integral auto hi) {
return val < lo ? lo : val > hi ? hi : val;
}Abbreviated templates are best for leaf functions with one or two parameters where the type name is not needed elsewhere in the body or return type. For more complex signatures — multiple parameters with shared types, recursive templates, or partial specialisations — prefer the explicit template<Concept T> form.
Concept Checking in static_assert
static_assert with a concept is the preferred way to document and enforce type requirements at the point of instantiation. Unlike a constraint on a template, a static_assert inside a class or function body fires immediately with your message rather than a substitution failure deep in the error log.
// C++20
#include <concepts>
struct MyType {
MyType() = default;
MyType(const MyType&) = default;
MyType(MyType&&) = default;
MyType& operator=(const MyType&) = default;
MyType& operator=(MyType&&) = default;
bool operator==(const MyType&) const = default;
};
// Document the intended semantic at the bottom of the header:
static_assert(std::regular<MyType>,
"MyType must satisfy std::regular (default-constructible, copyable, equality-comparable)");The same technique guards template specialisation bodies and gives readers a clear contract:
// C++20
template<typename T>
class Registry {
static_assert(std::copyable<T>,
"Registry<T>: T must be copyable so entries can be cloned");
static_assert(std::equality_comparable<T>,
"Registry<T>: T must be equality-comparable for duplicate detection");
public:
void insert(const T& value);
bool contains(const T& value) const;
};Standard type-traits have concept equivalents that read more naturally in assertions:
// C++20
// Old style (still valid):
static_assert(std::is_nothrow_move_constructible_v<T>);
// Concept style (more composable, participates in subsumption):
static_assert(std::nothrow_move_constructible<T>,
"T must have a noexcept move constructor for use in this container");Place concept static_asserts in headers alongside the template definition, not in .cpp files. They act as regression tests: if a future change to MyType removes copy semantics, the assertion fires immediately with a clear message.
Writing Concept-Based Libraries
Good library concepts are semantic, not just syntactic. A concept that only checks required operations without documenting their intended meaning is a type-syntax checker, not a contract. State preconditions in prose; use the concept to enforce the syntactic minimum.
// C++20
#include <concepts>
#include <ranges>
#include <algorithm>
// Semantic intent: T supports operator< and it defines a strict weak order.
template<typename T>
concept LessThanComparable = requires(const T& a, const T& b) {
{ a < b } -> std::convertible_to<bool>;
};
// Sortable combines range, element-order, and iterator-permutability.
// Precondition (prose): the range has a defined strict weak order on its elements.
template<typename R>
concept MySortable =
std::ranges::random_access_range<R> &&
LessThanComparable<std::ranges::range_value_t<R>> &&
std::sortable<std::ranges::iterator_t<R>>;
// Library function: the concept is the entire documented interface.
template<MySortable R>
void library_sort(R& range) {
std::ranges::sort(range);
}Callers get a single, named error when the contract is violated rather than a cascade of failed substitutions inside std::ranges::sort.
// C++20
// Usage: concept requirements are checked at the call site.
std::vector<int> v{3, 1, 4, 1, 5};
library_sort(v); // OK
std::list<int> lst{3, 1, 4};
// library_sort(lst); // error: std::list iterator is not random_access
// -> constraint 'MySortable<std::list<int>>' not satisfied
// -> 'std::ranges::random_access_range<std::list<int>>' is false
// Verify the concept boundary in the library's test suite:
static_assert( MySortable<std::vector<int>>);
static_assert(!MySortable<std::list<int>>);
static_assert(!MySortable<std::vector<std::vector<int>>>); // no operator<Design principle: name every semantic property as a separate concept, compose them with &&, and document the prose contract next to each concept definition. Users who read only the concept list understand what the library expects; users who read the prose understand why.
See Also
- Concepts — Basics — fundamental syntax and the standard
<concepts>catalogue - Ranges and views — the standard library's largest consumer of concept hierarchies
- CRTP — the pre-C++20 static polymorphism technique concepts largely supersede
- Deducing
this(C++23) — explicit object parameters pair naturally with concept constraints on member functions