Skip to content
C++
Language
since C++20
Advanced

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++20

Concepts 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:

cpp
// 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

cpp
// 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.

cpp
// 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: same

The 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:

cpp
// 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 fire

Tiered 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:

cpp
// 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:

cpp
// 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 satisfied

Building algebraic concept hierarchies

cpp
// 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

cpp
// 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

cpp
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.

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