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

Concepts and requires Expressions

C++20 concepts and requires expressions in depth — four requirement forms, concept definition, subsumption rules, and overload resolution.

requires expressionsince C++20

A compile-time boolean expression that probes whether a set of expressions or types are syntactically well-formed, returning true if every requirement is satisfied and false otherwise without causing a hard error.

Overview

C++20 introduced two related constructs that share the requires keyword:

  • requires clause — a constraint predicate gating template instantiation: template<typename T> requires Predicate<T>
  • requires expression — a compile-time boolean that probes expression validity: requires(T x) { x.begin(); }

A concept is a named, reusable constraint (typically a requires expression) bound to a template parameter with the concept keyword. Concepts compose, participate in subsumption-based overload resolution, and produce far better diagnostics than raw SFINAE ever could.

The double requires requires idiom is intentional and valid: the first is the clause, the second is the expression. This is a common source of confusion but not an error.

Syntax

Four Requirement Kinds Inside a requires Expression

cpp
// 1. Simple requirement — expression must be syntactically valid
requires (T a, T b) {
    a + b;       // T supports binary +
    a.begin();   // T has a member begin()
    T{};         // T is default-constructible
}

// 2. Type requirement — a nested type or template instantiation must be valid
requires {
    typename T::value_type;              // nested type must exist
    typename T::allocator_type;
    typename std::iterator_traits<T*>;   // template instantiation must succeed
}

// 3. Compound requirement — expression valid + return type satisfies concept
requires (T a) {
    { a.size()  } -> std::convertible_to<std::size_t>;   // C++20
    { a.begin() } -> std::input_or_output_iterator;       // C++20
    { a.clone() } noexcept -> std::same_as<T>;            // noexcept + type
}

// 4. Nested requirement — an inner constraint must hold
requires (T a) {
    requires std::is_trivially_copyable_v<T>;
    requires sizeof(T) <= 16;
}

All four kinds may appear freely mixed within a single requires expression.

Defining Concepts

cpp
// C++20
template<typename T>
concept Printable = requires(T x) {
    { std::cout << x } -> std::same_as<std::ostream&>;
};

template<typename T>
concept Container = requires(T c, const T cc) {
    typename T::value_type;
    typename T::iterator;
    typename T::const_iterator;
    { c.begin()  } -> std::same_as<typename T::iterator>;
    { cc.begin() } -> std::same_as<typename T::const_iterator>;
    { c.size()   } -> std::convertible_to<std::size_t>;
    { c.empty()  } -> std::convertible_to<bool>;
};

// Concepts compose with && and || directly
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

template<typename T>
concept SizedContiguousRange =
    std::ranges::contiguous_range<T> &&  // C++20
    std::ranges::sized_range<T>;          // C++20

Applying Concepts — Four Equivalent Syntaxes

cpp
// 1. requires clause after template head — scales to complex multi-constraints
template<typename T>
    requires Container<T>
void process(T& c);

// 2. Constrained template parameter
template<Container T>
void process(T& c);

// 3. Abbreviated function template (C++20)
void process(Container auto& c);

// 4. Trailing requires clause
template<typename T>
void process(T& c) requires Container<T>;

Style 3 is the most concise for single-parameter functions. Style 1 is preferred when multiple type parameters carry separate constraints; keeping the requires clause on its own line makes it unambiguous and scannable.

Examples

Arithmetic Range Algorithm

cpp
#include <concepts>
#include <ranges>
#include <vector>
#include <list>

template<typename T>
concept Arithmetic = std::integral<T> || std::floating_point<T>;  // C++20

template<typename T>
concept ArithmeticRange =
    std::ranges::input_range<T> &&
    Arithmetic<std::ranges::range_value_t<T>>;  // C++20

template<ArithmeticRange R>
auto range_sum(const R& r) -> std::ranges::range_value_t<R> {
    using V = std::ranges::range_value_t<R>;
    V total{};
    for (const auto& v : r) total += v;
    return total;
}

std::vector<double> v{1.5, 2.5, 3.0};
auto s = range_sum(v);   // double — OK

std::list<int> l{1, 2, 3};
auto t = range_sum(l);   // int — OK

range_sum(std::string{"hi"});
// Error: constraint not satisfied — char is not Arithmetic

Subsumption-Driven Overload Resolution

Subsumption fires when one concept's constraints are a provable superset of another's — specifically when the normalized atomic constraints of concept A appear within concept B via logical conjunction. This enables the compiler to pick the more constrained overload without ambiguity.

cpp
// C++20
template<typename T>
concept Hashable = requires(T x) {
    { std::hash<T>{}(x) } -> std::convertible_to<std::size_t>;
};

// OrderedHashable subsumes Hashable via conjunction
template<typename T>
concept OrderedHashable = Hashable<T> && std::totally_ordered<T>;  // C++20

template<Hashable T>
void insert(T key) { /* hash-only container path */ }

template<OrderedHashable T>
void insert(T key) { /* can also binary-search; preferred when available */ }

insert(42);                 // OrderedHashable<int> — second overload wins
insert(std::string{"x"});   // OrderedHashable<std::string> — second overload

Subsumption checks atomic constraint identity, not semantic equivalence. Two independently named concepts with identical bodies do not subsume each other — the compiler cannot prove one implies the other without structural composition.

Constraining Class Templates and Non-Template Members

cpp
// C++20
template<typename T>
    requires std::is_trivially_copyable_v<T>
class Ring {
    T data_[64];
public:
    // Non-template members can also carry requires clauses (C++20)
    void push(T val)       requires std::copyable<T>;
    void push(T&& val)     requires std::movable<T>;
    T    pop()             requires std::default_initializable<T>;
};

Ad-Hoc Inline Constraint (requires-requires)

Use the double form when the constraint is specific to one callsite and not worth naming:

cpp
// C++20
template<typename T>
    requires requires(T& x, std::size_t n) { x.reserve(n); }
void preallocate(T& c, std::size_t n) {
    c.reserve(n);
}

// If this constraint appears in more than one template, extract it:
template<typename T>
concept Reservable = requires(T& x, std::size_t n) { x.reserve(n); };

Best Practices

Prefer named concepts over inline requires expressions for any constraint that is referenced more than once. Named concepts participate in subsumption; structurally identical but separately defined expressions do not.

Use compound requirements with the weakest satisfying concept for return types. -> std::convertible_to<bool> is more general and usually more correct than -> std::same_as<bool> for predicates like empty().

Validate your concept assumptions with static_assert before shipping:

cpp
static_assert(std::ranges::random_access_range<std::vector<int>>);   // C++20
static_assert(!std::ranges::random_access_range<std::list<int>>);    // C++20
static_assert(Container<std::vector<double>>);
static_assert(!Container<int>);

Prefer requires clause on a separate line from the template head for multi-constraint templates. It visually separates the parameter declaration from the constraint, which aids code review.

Common Pitfalls

Renamed Concepts Break Subsumption

cpp
template<typename T>
concept A = std::integral<T>;

template<typename T>
concept B = std::integral<T>;  // same body, different name

template<A T> void f(T);
template<B T> void f(T);  // ambiguous — A and B do NOT subsume each other

The compiler compares atomic constraint expressions by identity (same template, same arguments), not by logical equivalence. Subsumption is structural, not semantic.

requires Expressions Check Syntax, Not Correctness

A requires expression returning true means the expressions are well-formed at the point of checking — it says nothing about whether the operation is semantically meaningful or produces the right answer:

cpp
template<typename T>
concept HasSize = requires(T x) { x.size(); };
// Satisfied even if size() returns void or always throws

Guard against semantic misuse with compound requirements that constrain return types, not just expression validity.

Negation Does Not Invert Subsumption

Negating a concept in a requires clause is legal but creates an unconstrained overload that cannot be subsumed:

cpp
template<typename T>
    requires (!std::integral<T>)  // not part of any subsumption chain
void f(T);

If you need mutually exclusive overloads, compose positive concepts explicitly rather than negating.

Compound Return Type Constraint Syntax

In { expr } -> Concept, the Concept is checked against the expression's type as if calling Concept<decltype((expr))> — the type is passed as the first argument. std::same_as<T> therefore matches the exact type including value category; std::convertible_to<T> is almost always the right choice for output constraints unless you need exact identity.

requires-expression inside requires-clause

A requires expression can appear directly inside a requires clause without naming a concept. This is the requires requires double form — the outer requires is the clause gating the template, and the inner requires begins the expression being evaluated.

cpp
// C++20
// Inline constraint: T must support .push_back() taking a value_type
template<typename T>
    requires requires(T& c, typename T::value_type v) {
        c.push_back(v);
    }
void append_one(T& container, typename T::value_type val) {
    container.push_back(val);
}

// When the same expression recurs, extract it into a named concept instead
template<typename T>
concept PushBackable = requires(T& c, typename T::value_type v) {
    c.push_back(v);
};

template<PushBackable T>
void append_one(T& container, typename T::value_type val) {  // C++20
    container.push_back(val);
}

The inline form is useful for one-off constraints at a single callsite, but named concepts should be preferred the moment the same predicate is needed in more than one template — only named concepts participate in subsumption.

Compound requirements

Compound requirements check both that an expression is valid and that its return type satisfies a concept. The syntax { expr } -> ConceptName desugars to: first evaluate expr without triggering a hard error; then check ConceptName<decltype((expr))>.

cpp
// C++20
template<typename T>
concept Sizeable = requires(const T& c) {
    // expression must be valid AND return type must be convertible to size_t
    { c.size() } -> std::convertible_to<std::size_t>;
};

template<typename T>
concept CloneableNoexcept = requires(const T& x) {
    // noexcept specifier makes the constraint also require a non-throwing call
    { x.clone() } noexcept -> std::same_as<T>;  // C++20
};

template<typename T>
concept Iterator = requires(T it) {
    { *it   } -> std::same_as<typename T::reference>;
    { ++it  } -> std::same_as<T&>;
    { it++  } -> std::same_as<T>;
};

Prefer std::convertible_to<R> over std::same_as<R> for return-type constraints unless you need exact type identity — convertible_to passes through implicit conversions and is almost always the correct choice for library interfaces.

Nested requirements

A nested requirement embeds an arbitrary constant expression as an additional constraint inside a requires expression. Use the requires keyword again as the third "requires" form: requires ConstantExpression; inside the body of a requires expression.

cpp
// C++20
template<typename T>
concept SmallTriviallyCopyable = requires {
    requires std::is_trivially_copyable_v<T>;  // nested: trait must hold
    requires (sizeof(T) <= 16);                // nested: size limit
};

template<typename T>
concept SignedArithmetic = requires(T a, T b) {
    a + b;
    a - b;
    a * b;
    requires std::signed_integral<T> || std::floating_point<T>;  // nested
};

// Practical use: constrain on the range's value type inside one concept
template<typename R>
concept FloatRange =
    std::ranges::input_range<R> &&
    requires {
        requires std::floating_point<std::ranges::range_value_t<R>>;  // C++20
    };

Nested requirements are evaluated as ordinary constant expressions; if one is ill-formed as a constant expression the whole requires expression produces false, not a hard error.

Concept subsumption and partial ordering

Subsumption allows the compiler to rank overloads by constraint strength without ambiguity. Concept A subsumes concept B when every atomic constraint in B's normalized form also appears in A's normalized form under logical conjunction — meaning A's constraint set is provably a superset of B's.

cpp
// C++20
template<typename T>
concept Hashable = requires(T x) {
    { std::hash<T>{}(x) } -> std::convertible_to<std::size_t>;
};

template<typename T>
concept HashableAndOrdered = Hashable<T> && std::totally_ordered<T>;
//  ^^^ subsumes Hashable because Hashable<T> is a conjunct in its definition

template<Hashable T>
std::size_t bucket(T key);  // less constrained

template<HashableAndOrdered T>
std::size_t bucket(T key);  // more constrained — selected when both are satisfied

bucket(42);           // int satisfies both; HashableAndOrdered overload wins
bucket(std::string{"x"});  // same — more constrained overload wins

Subsumption is structural, not semantic. Two concepts with identical bodies but different names do not subsume each other; only explicit syntactic composition via && or || establishes a subsumption relationship.

cpp
// C++20 — subsumption failure due to aliasing without composition
template<typename T>
concept EvenInt = std::integral<T>;  // copy of body, not composition

template<typename T>
concept OddInt  = std::integral<T>;  // same body

template<EvenInt T> void f(T);
template<OddInt  T> void f(T);  // ambiguous — neither subsumes the other

Abbreviated function templates

The auto placeholder in a function parameter list creates an implicit template parameter, and pairing it with a concept name produces a concise constrained function template. This abbreviated syntax is equivalent to a full explicit template declaration with a constrained parameter.

cpp
// C++20 — all three declarations are equivalent
template<std::integral T>
T increment(T x) { return x + 1; }

template<typename T>
    requires std::integral<T>
T increment(T x) { return x + 1; }

std::integral auto increment(std::integral auto x) { return x + 1; }
//  ^^^^^^^^^^^                ^^^^^^^^^^^^^^^^^
//  return type constrained     parameter constrained

// Multiple auto parameters — each gets its own independent implicit T
void swap_print(std::totally_ordered auto a, std::totally_ordered auto b) {
    if (a > b) std::swap(a, b);
    std::cout << a << ' ' << b << '\n';
}
// Note: a and b may be DIFFERENT types — use explicit template<T> if they must match

The abbreviated form shines for single-parameter utilities and lambdas; for multi-parameter templates where type relationships matter, the explicit template<typename T> form avoids surprising independent deduction.

cpp
// C++20 — constrained lambda using abbreviated syntax
auto square = [](std::arithmetic auto x) { return x * x; };
//  std::arithmetic is not in the standard — use a user-defined concept or:
auto square2 = [](auto x) requires (std::integral<decltype(x)> ||
                                    std::floating_point<decltype(x)>) {
    return x * x;
};

Constraining class templates

A requires clause after the template<> head constrains every instantiation of a class template. Member functions and member function templates can carry their own additional requires clauses that narrow their availability further.

cpp
// C++20
template<typename T>
    requires std::is_trivially_destructible_v<T>
class FixedBuffer {
    T data_[128];
public:
    // Member available only when T is also copy-constructible
    void fill(const T& val) requires std::copy_constructible<T>;

    // Member available only when T is move-constructible
    void fill(T&& val)      requires std::move_constructible<T>;

    // Non-type template member with its own constraint
    template<std::size_t N>
        requires (N <= 128)
    void copy_to(T (&arr)[N]) const;
};

// Specializations may relax or tighten constraints independently
template<std::integral T>
class FixedBuffer<T> {
    // Integer-specific specialization — different set of methods
};

Class template constraints are checked at the point of instantiation, not at the point of declaration. An unconstrained member function template inside a constrained class is only reachable when the class itself is already instantiated, so the class constraint is always evaluated first.

Concepts in standard library (std::ranges)

The <concepts> and <ranges> headers ship a rich taxonomy of pre-defined concepts that cover the vast majority of generic programming needs. Prefer these over hand-rolled equivalents whenever possible; they are carefully specified and participate in subsumption chains across the entire standard library.

cpp
// C++20 — core language concepts from <concepts>
static_assert(std::same_as<int, int>);
static_assert(std::derived_from<std::vector<int>, std::vector<int>>);
static_assert(std::convertible_to<int, double>);
static_assert(std::integral<long>);
static_assert(std::floating_point<float>);
static_assert(std::copyable<std::string>);
static_assert(std::movable<std::unique_ptr<int>>);
static_assert(std::regular<int>);       // copyable + equality_comparable
static_assert(std::semiregular<int>);   // copyable + default_initializable
cpp
// C++20 — range concepts from <ranges>
#include <ranges>
#include <vector>
#include <list>
#include <forward_list>

static_assert(std::ranges::random_access_range<std::vector<int>>);
static_assert(std::ranges::bidirectional_range<std::list<int>>);
static_assert(std::ranges::forward_range<std::forward_list<int>>);
static_assert(std::ranges::contiguous_range<std::vector<int>>);
static_assert(std::ranges::sized_range<std::vector<int>>);
static_assert(!std::ranges::sized_range<std::forward_list<int>>);

// range_value_t, range_reference_t, range_difference_t work with any range
using V = std::ranges::range_value_t<std::vector<double>>;  // double  // C++20
cpp
// C++20 — algorithm constraint example using std::ranges concepts
template<std::ranges::input_range R,
         std::indirect_unary_predicate<std::ranges::iterator_t<R>> Pred>
std::ranges::range_difference_t<R> count_if(R&& range, Pred pred) {
    std::ranges::range_difference_t<R> n = 0;
    for (auto&& elem : range)
        if (pred(elem)) ++n;
    return n;
}

The std::ranges concepts form a strict hierarchy — random_access_range refines bidirectional_range, which refines forward_range, and so on — so subsumption automatically selects the most specialized algorithm overload for a given range type.

See Also

  • reference/language/concepts — concept keyword, basic definition and satisfaction rules
  • reference/language/concepts-advanced — partial specialization with concepts, concept maps, variable template constraints
  • reference/language/templates — SFINAE and pre-C++20 enable_if techniques
  • reference/language/ranges — std::ranges concepts taxonomy in practice