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

Concepts (C++20)

C++20 concepts are named, composable boolean predicates on template parameters that replace SFINAE with readable constraints and precise error messages.

Conceptssince C++20

A concept is a named boolean predicate evaluated at compile time that constrains template parameters, enabling readable constraint expressions, clean overload resolution, and informative error messages in place of SFINAE substitution failures.

Overview

Before C++20, constraining template parameters meant either documentation-only (hoping callers read it) or SFINAE β€” a mechanism so hostile to human readers that entire libraries existed solely to make it writable. Concepts replace both with a first-class language feature.

A concept names a constraint. The compiler checks it before instantiation, so violations produce a message that names the unsatisfied concept rather than a wall of substitution failures. Concepts also participate in overload resolution through subsumption: the most-constrained viable overload is preferred, without hacks like tag dispatch or enable_if chains.

Concepts interact closely with three other C++20 features: requires clauses (attach constraints to existing templates), requires expressions (test whether operations are syntactically valid), and abbreviated function templates (void f(Concept auto x)). All four are distinct constructs that compose.

Syntax

Defining a concept

cpp
// Concept definition β€” C++20
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;  // C++20

// requires expression inside a concept
template<typename T>
concept Container = requires(T c) {
    { c.begin() } -> std::input_iterator;          // compound requirement
    { c.end()   } -> std::input_iterator;
    { c.size()  } -> std::convertible_to<size_t>;
    typename T::value_type;                        // type requirement
};

// Combining concepts with && and ||
template<typename T>
concept SortableContainer = Container<T>
    && std::sortable<std::ranges::iterator_t<T>>;  // C++20 ranges

Four ways to apply a concept

cpp
// 1. Constrained template parameter β€” most common
template<std::integral T>
void print_bits(T value);

// 2. requires clause β€” use when the constraint is complex or conditional
template<typename T>
    requires std::integral<T> && (sizeof(T) >= 4)
void print_bits(T value);

// 3. Abbreviated function template β€” C++20 shorthand, equivalent to (1)
void print_bits(std::integral auto value);

// 4. Constrained auto in variable or return type
std::integral auto result = compute_id();  // deduced type must satisfy std::integral

All four are semantically equivalent for simple cases. Prefer (1) when the parameter name is needed elsewhere in the signature; (3) for one-liner lambdas and short functions.

Requires expression anatomy

A requires expression tests syntactic validity β€” it is itself a bool constant expression usable anywhere a constant expression is expected.

cpp
// requires expression β€” C++20
requires(T a, T b) {
    // Simple requirement: expression must be well-formed
    a + b;

    // Type requirement: nested type must exist
    typename T::value_type;

    // Compound requirement: expression valid AND return type satisfies concept
    { a + b } -> std::same_as<T>;
    { a.size() } -> std::convertible_to<std::size_t>;

    // noexcept requirement: expression must be declared noexcept
    { a.swap(b) } noexcept;

    // Compound + noexcept together
    { a.clone() } noexcept -> std::same_as<T>;
};

The -> in a compound requirement takes a concept, not a type. { expr } -> std::same_as<int> checks that the expression's type satisfies same_as<int>, which means the type is exactly int. Use std::convertible_to<int> to accept anything implicitly convertible.

Examples

Overloading without enable_if

Concepts enable clean, ordered overloads. The compiler picks the most-constrained matching overload β€” subsumption β€” without explicit priority tricks.

cpp
#include <concepts>
#include <print>

// C++20: overloads ranked by constraint specificity
template<std::integral T>
void describe(T x) { std::println("integral: {}", x); }    // C++23 std::println

template<std::floating_point T>
void describe(T x) { std::println("floating: {}", x); }

template<std::arithmetic T>   // integral || floating_point
void describe(T x) { std::println("arithmetic: {}", x); }  // never reached β€” subsumed

template<typename T>
void describe(T x) { std::println("other"); }

// describe(42)    -> "integral: 42"
// describe(3.14)  -> "floating: 3.14"
// describe("hi")  -> "other"

std::arithmetic<T> is subsumed by both std::integral<T> and std::floating_point<T>, so it is never the most-constrained match when either of those applies.

Writing a real concept: Hashable key

cpp
#include <concepts>
#include <functional>

template<typename K>
concept HashMapKey =
    std::regular<K>               // copyable, equality-comparable, default-constructible
    && requires(K k) {
        { std::hash<K>{}(k) } noexcept -> std::convertible_to<std::size_t>;
    };

template<HashMapKey K, typename V>
class FlatMap { /* ... */ };

FlatMap<std::string, int>  table1;  // OK
FlatMap<std::vector<int>, int> t2;  // error: vector has no std::hash specialization

Recursive and dependent concepts

cpp
#include <concepts>
#include <ranges>

// Concept constraining a range whose elements themselves satisfy a concept
template<typename R, typename C>
concept RangeOf = std::ranges::range<R>
    && C<std::ranges::range_value_t<R>>;

// Use: accept ranges of integral values only
template<typename R>
    requires RangeOf<R, std::integral>
auto sum(R&& range) {
    std::ranges::range_value_t<R> total{};
    for (auto v : range) total += v;
    return total;
}

Concept-constrained lambdas (C++20)

cpp
auto square = []<std::arithmetic T>(T x) { return x * x; };

// Or with abbreviated syntax:
auto square = [](std::arithmetic auto x) { return x * x; };

Best Practices

Name concepts at the right granularity. A concept like HasSize that only checks c.size() is less useful than std::ranges::sized_range, which captures the full semantic contract. Favour reusing standard concepts and composing them over writing one-off structural checks.

Prefer std::same_as over raw type checks in compound requirements. { expr } -> std::same_as<T> is explicit about exact type. If you write { a + b } with no -> annotation, the return type is unconstrained β€” the requirement only checks that a + b compiles.

Use requires clauses for multi-parameter constraints. When a constraint spans multiple template parameters or involves relationships between them, a requires clause is clearer than cramming everything into individual parameter constraints.

cpp
// Relationship constraint β€” requires clause is the right tool
template<typename From, typename To>
    requires std::convertible_to<From, To> && (!std::same_as<From, To>)
To convert(From x);

Prefer naming a concept over inlining a requires expression in a clause. Unnamed requires requires double-keywords are valid but obscure β€” name the concept instead so the constraint is reusable and self-documenting.

Common Pitfalls

Concepts verify syntax, not semantics

A concept satisfied by structural checking cannot enforce the semantic contract. A type can have operator+ that violates mathematical associativity and still satisfy Addable. Concepts narrow down plausible types; correctness remains the programmer's responsibility.

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

struct Broken {
    Broken operator+(Broken) const { return {}; }  // always returns default
};
static_assert(Addable<Broken>);  // passes β€” syntax only

The double requires is syntactically valid but rarely wanted

cpp
// valid but confusing β€” outer requires clause, inner requires expression
template<typename T>
    requires requires(T t) { t.foo(); }
void f(T t);

// better β€” name it
template<typename T>
concept HasFoo = requires(T t) { t.foo(); };

template<HasFoo T>
void f(T t);

Subsumption only works through concept definitions, not raw expressions

The compiler can only subsume constraints it can decompose into named atomic concepts. Two structurally identical requires expressions in separate clauses are not subsumed β€” they are treated as distinct, unordered constraints, causing ambiguity.

cpp
// WRONG β€” ambiguous, no subsumption between raw requires expressions
template<typename T>
    requires requires(T t) { t.begin(); }
void g(T t);

template<typename T>
    requires requires(T t) { t.begin(); } && requires(T t) { t.size(); }
void g(T t);  // NOT more constrained than the above β€” ambiguous call

// RIGHT β€” use named concepts so subsumption can apply
template<std::ranges::range T>
void g(T t);

template<std::ranges::sized_range T>  // sized_range subsumes range
void g(T t);  // unambiguously more constrained

-> ConceptName takes a unary concept, not a type

{ expr } -> int is a compile error. The -> syntax requires a concept: { expr } -> std::same_as<int> or { expr } -> std::convertible_to<int>.

See Also