Concepts and Constraints

This section documents C++23 (ISO/IEC 14882:2024), as published by ISO/IEC JTC1/SC22/WG21 (wg21), verified against the freely available working draft N5046 (eel.is/c++draft) and cppreference.com.

This content was generated with the assistance of AI and should be verified against the working draft and cppreference.com before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Before C++20, a template argument that didn’t satisfy a function’s implicit expectations produced a wall of cryptic errors deep inside the template’s body. Concepts name those expectations and check them before instantiation, turning that wall into one clear message at the call site.

concept and requires Clauses

#include <concepts>

template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;   // a named, reusable predicate on types

template <typename T>
requires Numeric<T>                       // a requires clause: constrains this overload to Numeric types
T doubleIt(T value) {
    return value * 2;
}

template <Numeric T>                       // equivalent, more concise: the concept used directly as the
T doubleItV2(T value) {                     // template-parameter's "type"
    return value * 2;
}

Calling doubleIt("hello") now fails with "constraints not satisfied: Numeric<const char*>" at the call site, instead of a multi-page error from inside `doubleIt’s body.

requires Expressions

A requires expression (as opposed to a requires clause, which uses one) directly describes what syntax must be valid, for ad hoc constraints with no pre-existing named concept:

template <typename T>
concept Printable = requires(T value, std::ostream& os) {
    { os << value } -> std::same_as<std::ostream&>;   // "os << value" must compile and yield exactly ostream&
};

template <typename T>
concept Container = requires(T c) {
    typename T::value_type;         // a nested type must exist
    c.begin();                       // these expressions must be valid
    c.end();
    { c.size() } -> std::convertible_to<std::size_t>;
};

Standard Concepts

<concepts> ships a library of ready-made concepts covering the most common constraints, so most code never needs to hand-write one:

Concept Constrains to

std::integral<T>

Any integer type (int, long, bool, char, …​).

std::floating_point<T>

float, double, long double.

std::same_as<T, U>

Exactly the same type (order-sensitive; same_as<T,U> and same_as<U,T> are both checked).

std::convertible_to<From, To>

From implicitly converts to To.

std::default_initializable<T>

T can be default-constructed.

std::copyable<T> / std::movable<T>

T supports copy / move.

std::regular<T>

Default-constructible, copyable, and equality-comparable — a "well-behaved value type".

Abbreviated Function Templates

C++20 lets a function parameter’s type itself be auto (optionally constrained), implicitly making the function a template with no template<…​> header needed:

auto add(auto a, auto b) {              // implicitly: template<typename T, typename U> auto add(T a, U b)
    return a + b;
}

auto addNumeric(Numeric auto a, Numeric auto b) {   // constrained abbreviated template
    return a + b;
}

Subsumption

When two constrained overloads could both match, the compiler prefers the one whose constraints are strictly more specific — this ordering is called subsumption:

template <typename T>
requires std::integral<T>
void process(T) { /* generic integral handling */ }

template <typename T>
requires std::integral<T> && std::signed_integral<T>
void process(T) { /* signed-specific handling */ }

// process(5) calls the SECOND overload: std::signed_integral<T> && std::integral<T>
// subsumes (is more specific than) std::integral<T> alone, so it wins overload resolution
// instead of being ambiguous.

Constraint-Check Flow

flowchart TD A["Call site: doubleIt(value)"] --> B["Candidate overloads collected"] B --> C{"For each candidate:
evaluate its constraints"} C -->|"satisfied"| D["Candidate stays viable"] C -->|"not satisfied"| E["Candidate discarded --
no instantiation attempted"] D --> F{"More than one
viable candidate?"} F -->|"no"| G["Chosen overload is instantiated"] F -->|"yes"| H["Subsumption: most-specific
constraint set wins"] H --> G E --> I["All candidates discarded?"] I -->|"yes"| J["Compile error at the call site,
naming the unsatisfied constraint"]