Polyglot
Languages C++ concepts
C++ § concepts

Concepts

Concepts (C++20) are named, reusable constraints on template parameters. A concept is a compile-time predicate over a type or a set of types; it can appear in template parameter lists, in requires-clauses, in the auto keyword to constrain otherwise-unconstrained type deduction, and in if constexpr branches. The mechanism is the standard’s response to the long-standing problems of template error-message complexity (a typo in a generic call could produce hundreds of lines of unintelligible diagnostics) and the awkward SFINAE idioms that pre-C++20 generic code relied on. Concepts make constraints explicit, reusable, and diagnosable.

The motivation

Pre-C++20 templates accept any type that, in the body, is valid for the operations the template performs. The constraint is implicit: the template body uses <, +, member functions, etc., and the substitution succeeds or fails on those operations.

template <typename T>
T sum(const std::vector<T> &v) {
    T total = T{};
    for (const auto &x : v) total = total + x;
    return total;
}

sum<MyType> works if MyType is default-constructible and supports operator+. Any type for which the body fails to substitute produces a compilation error — typically a long, deeply-nested message about which expression in which template instantiation failed.

Concepts make the constraint explicit:

template <typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::convertible_to<T>;
    T{};
};

template <Addable T>
T sum(const std::vector<T> &v) {
    T total = T{};
    for (const auto &x : v) total = total + x;
    return total;
}

The constraint Addable is checked at the call site, not deep inside the template body. A type that does not satisfy Addable produces a diagnostic naming Addable and the missing operation directly.

Defining concepts

A concept is defined with concept:

template <typename T>
concept Numeric = std::is_arithmetic_v<T>;

template <typename T>
concept Comparable = requires(T a, T b) {
    { a == b } -> std::convertible_to<bool>;
    { a != b } -> std::convertible_to<bool>;
    { a <  b } -> std::convertible_to<bool>;
    { a >  b } -> std::convertible_to<bool>;
};

The body of a concept is a constraint expression: a compile-time boolean. The simplest forms use type traits (std::is_arithmetic_v<T>); more elaborate forms use requires-expressions to test for the presence of operations.

requires-expressions

A requires-expression evaluates to true if every requirement inside is satisfied for the named types:

template <typename T>
concept Drawable = requires(T t, std::ostream &os) {
    t.draw(os);                                 // expression must compile
    { t.size() } -> std::convertible_to<int>;   // expression compiles, type constraint
    typename T::shape_kind;                      // T has a nested type
    requires sizeof(T) <= 256;                   // arbitrary boolean
};

The four kinds of requirement:

FormMeaning
expr;The expression must be valid (compile).
{ expr } -> Constraint;The expression must be valid, and its type must satisfy Constraint.
typename T::name;T::name must name a type.
requires expr;The (compile-time boolean) expression must be true.

The expressions inside the requires-block are not evaluated; only their well-formedness is checked.

requires-clauses

A requires-clause attaches a constraint to a template:

template <typename T>
T sum(const std::vector<T> &v) requires Addable<T> {
    /* … */
}

template <typename T> requires Numeric<T>
T square(T x) { return x * x; }

Two equivalent placements: at the end of the function (after the parameter list) or before the function declaration. The first form is more common for the trailing constraints; the second is more common for parameter-related constraints.

Concepts as type-parameter constraints

The most concise form replaces typename in the parameter list:

template <Addable T>
T sum(const std::vector<T> &v) { /* … */ }

Addable T is shorthand for typename T requires Addable<T>. The form is the conventional choice when a single concept constrains a parameter.

For multiple constraints, the requires-clause is more readable:

template <typename T>
    requires Numeric<T> && Comparable<T>
T pick_smaller(T a, T b);

Concepts in the auto keyword

A concept may constrain auto:

auto first(const std::vector<int> &v) -> Numeric auto {
    return v.front();
}

void process(Drawable auto &d) {
    d.draw(std::cout);
}

auto x = std::vector<int>{};   // unconstrained
Drawable auto d = make_widget(); // constrained

The Drawable auto form is the constrained-auto shorthand: it permits any type that satisfies Drawable. Function parameters declared with constrained auto introduce abbreviated function templates — the compiler synthesises the template parameter list:

void process(Drawable auto &d);
// equivalent to:
template <Drawable T>
void process(T &d);

The construction is the conventional way to declare simple generic functions in modern code.

Standard concepts

The <concepts> header defines a set of standard concepts:

ConceptTests
std::same_as<T, U>T and U are the same type
std::convertible_to<T, U>T is convertible to U
std::derived_from<D, B>D derives from B
std::integral<T>T is an integer type
std::signed_integral<T>T is a signed integer type
std::floating_point<T>T is a floating-point type
std::default_initializable<T>T may be default-initialised
std::copy_constructible<T>T may be copy-constructed
std::move_constructible<T>T may be move-constructed
std::equality_comparable<T>T supports == and !=
std::totally_ordered<T>T supports <, <=, >, >=, ==, !=
std::invocable<F, Args...>F may be called with Args
std::predicate<F, Args...>F returns a bool-convertible value
std::regular<T>T is regular (default-constructible, copyable, equality-comparable)
std::semiregular<T>T is regular but without the equality requirement

Additional concepts cover iterators (std::input_iterator, std::random_access_iterator), ranges, and the standard algorithms’ constraints.

Concept-based overload resolution

Concepts integrate with overload resolution: when two overloads differ in their constraints, the more constrained one is selected when both apply. The “more constrained” relation is defined by subsumption: a concept A subsumes B if A’s constraint expression implies B’s.

template <typename T>
void f(T x) {
    /* generic */
}

template <std::integral T>
void f(T x) {
    /* integer-specialised */
}

f(3);          // calls the integral overload (more constrained)
f(3.14);       // calls the generic overload (only one viable)

The mechanism replaces the enable_if/SFINAE patterns of pre-C++20 code, where the same effect required substantially more boilerplate.

The relationship to SFINAE

SFINAE — Substitution Failure Is Not An Error — was the pre-C++20 mechanism for constraint-based overload selection. The pattern:

template <typename T,
          typename = std::enable_if_t<std::is_integral_v<T>>>
void f(T x);

The same constraint, with concepts:

template <std::integral T>
void f(T x);

The concepts version is shorter, produces better diagnostics, and integrates cleanly with the rest of the language. SFINAE remains legal — large bodies of pre-C++20 code use it — but new code should use concepts where they apply.

The cases where SFINAE remains useful are narrow: extremely fine-grained selection that the concept system cannot easily express, or libraries that must support pre-C++20 compilers. For typical generic code, concepts are the correct mechanism.

Idiomatic use

The conventional patterns:

Constrain the public interface, not the implementation

template <std::ranges::range R>
auto sum(R &&r) {
    typename std::ranges::range_value_t<R> total{};
    for (auto &&x : r) total = total + x;
    return total;
}

The constraint std::ranges::range R is at the parameter level; the implementation can use auto and let the compiler propagate the type.

Compose concepts

template <typename T>
concept Container = std::ranges::range<T> &&
                    requires { typename T::value_type; };

template <typename T>
concept Sortable = std::random_access_iterator<typename T::iterator> &&
                   std::totally_ordered<typename T::value_type>;

Concepts are first-class predicates; conjunction (&&) and disjunction (||) compose them.

Constrain auto for clarity

auto compute(std::ranges::input_range auto &&data) {
    return std::ranges::distance(data);
}

The constraint on auto documents the function’s contract more clearly than an unconstrained generic parameter.

Use standard concepts before defining new ones

The <concepts>, <iterator>, and <ranges> headers cover most of the conventional cases. New concepts should be defined for genuine domain-specific constraints, not for things the standard library already names.

A note on adoption

Concepts require C++20. Compiler support is mature (GCC, Clang, MSVC all support them by 2022). Library support is similarly broad: the standard library’s containers, iterators, and ranges are constrained with concepts as of C++20. New code targeting C++20 or later should use concepts as the primary constraint mechanism; SFINAE should be reserved for pre-C++20 code or for the rare cases that concepts cannot easily express.