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

Concepts Library

The C++20 <concepts> header provides standard named constraints — std::integral, std::regular, std::invocable — replacing SFINAE boilerplate with readable, composable template interfaces.

Concepts Librarysince C++20

The <concepts> header defines a canonical set of named requirements — std::integral, std::regular, std::invocable, and others — that constrain template parameters directly, replacing ad-hoc enable_if patterns with semantically precise, compiler-enforced interfaces.

Overview

Before C++20, constraining templates meant either tag dispatch, std::enable_if chains, or detection-idiom machinery — all producing unintelligible diagnostics and difficult-to-reuse constraints. The standard concepts library codifies the requirements that library authors had been reimplementing independently for over a decade.

Every concept in <concepts> is defined using the same concept and requires facilities available to user code. There is no privileged compiler mechanism: std::integral<T> is std::is_integral_v<T> expressed as a concept. The value of the standard library's concepts is the semantic contract they communicate and the subsumption relationships they establish — not any special status.

The library divides its concepts into four groups:

GroupRepresentative concepts
Core languagesame_as, derived_from, convertible_to, integral, floating_point
Comparisonequality_comparable, totally_ordered, three_way_comparable
Objectmovable, copyable, semiregular, regular
Callableinvocable, predicate, relation, strict_weak_order

Iterator and range concepts (std::input_iterator, std::ranges::range, etc.) live in <iterator> and <ranges> respectively, not in <concepts>.

Syntax

A concept from <concepts> can appear in four syntactic positions:

cpp
#include <concepts>

// 1. requires-clause after the template head
template<typename T>
    requires std::integral<T>
T increment(T v) { return v + 1; }

// 2. constrained type-parameter (concept replaces 'typename')
template<std::integral T>
T decrement(T v) { return v - 1; }

// 3. abbreviated function template — concept constrains auto
std::integral auto halve(std::integral auto v) { return v / 2; }

// 4. as a building block inside a user concept
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

Forms 1 and 2 are equivalent in semantics and subsumption. Form 3 is subtler: each auto introduces a separate, independent template parameter. Calling halve(1, 2L) compiles because T1=int and T2=long are deduced independently — both satisfy std::integral. Use forms 1 or 2 whenever a single type must appear in multiple parameter positions.

Examples

Core type-category concepts

cpp
#include <concepts>
#include <bit>      // C++20

// std::integral<T>: char, short, int, long, bool, and their unsigned/signed variants
template<std::integral T>
T rotate_left(T value, int shift) {
    return std::rotl(value, shift);       // C++20
}

// std::floating_point<T>: float, double, long double only
template<std::floating_point T>
T lerp(T a, T b, T t) {
    return std::lerp(a, b, t);           // C++20
}

// Narrower variants for tighter interfaces
template<std::signed_integral T>   T abs_safe(T v) { return v < 0 ? -v : v; }
template<std::unsigned_integral T> T abs_safe(T v) { return v; } // no-op overload

static_assert(std::integral<int>);
static_assert(!std::integral<float>);
static_assert(std::signed_integral<int>);
static_assert(!std::signed_integral<unsigned>);
static_assert(std::floating_point<long double>);

Relationship and conversion concepts

cpp
#include <concepts>

// std::same_as<T, U>: symmetric — requires T≡U from both directions
template<typename T, typename U>
    requires std::same_as<T, U>
void bitwise_copy(const T& src, U& dst) {
    dst = src;
}

// std::derived_from<Derived, Base>: public, unambiguous inheritance
struct Shape {};
struct Circle : Shape {};
struct PrivateCircle : private Shape {};

static_assert(std::derived_from<Circle, Shape>);
static_assert(!std::derived_from<PrivateCircle, Shape>); // private base

// std::convertible_to<From, To>: implicit conversion must be valid
static_assert(std::convertible_to<int, double>);
static_assert(!std::convertible_to<double*, int*>);

// std::assignable_from<LHS, RHS>: LHS must be an lvalue reference type
static_assert(std::assignable_from<int&, long>);
// std::assignable_from<int, long>  -- ill-formed: LHS must be a reference

Object concept hierarchy

The object concepts form a lattice; each name in the chain subsumes all concepts above it:

cpp
destructible
  └─ constructible_from<T, Args...>
       ├─ default_initializable
       └─ move_constructible
            └─ copy_constructible
movable      = move_constructible + assignable_from<T&,T> + swappable
copyable     = movable + copy_constructible + assignable_from<T&,const T&>
semiregular  = copyable + default_initializable
regular      = semiregular + equality_comparable
cpp
#include <concepts>
#include <string>

static_assert(std::regular<int>);
static_assert(std::regular<std::string>);

struct UniqueHandle {
    UniqueHandle() = default;
    UniqueHandle(UniqueHandle&&) noexcept = default;
    UniqueHandle& operator=(UniqueHandle&&) noexcept = default;
    UniqueHandle(const UniqueHandle&) = delete;
    UniqueHandle& operator=(const UniqueHandle&) = delete;
};

static_assert(std::movable<UniqueHandle>);
static_assert(!std::copyable<UniqueHandle>);
static_assert(!std::semiregular<UniqueHandle>);

// Constraining on std::regular guarantees copy, default-construct, and ==
template<std::regular T>
T clone_and_mutate(T value, auto fn) {
    T copy = value;    // valid: copy_constructible is guaranteed
    fn(copy);
    return copy;
}

std::regular is the semantic baseline for value types. Algorithms parameterised on std::regular interoperate with the largest set of well-behaved types.

Callable concepts

cpp
#include <concepts>
#include <functional>  // std::invoke C++17
#include <vector>
#include <algorithm>

// std::invocable<F, Args...>: F(Args...) is a valid call expression
template<std::invocable<int, int> BinaryOp>
int apply(BinaryOp op, int a, int b) {
    return std::invoke(op, a, b);         // C++17
}

// std::predicate<F, Args...>: invocable returning a boolean-testable result
template<typename T, std::predicate<const T&> Pred>
std::vector<T> filter(const std::vector<T>& v, Pred pred) {
    std::vector<T> out;
    for (const auto& x : v)
        if (pred(x)) out.push_back(x);
    return out;
}

// std::strict_weak_order<R, T, U>: models a valid sort comparator
template<typename T, std::strict_weak_order<T, T> Cmp>
void stable_sort_with(std::vector<T>& v, Cmp cmp) {
    std::stable_sort(v.begin(), v.end(), cmp);
}

Composing concepts and constraint subsumption

cpp
#include <concepts>

template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

template<typename T>
concept SignedNumeric = std::signed_integral<T>;  // subsumes std::integral

// Subsumption drives overload selection — the more-constrained overload wins
template<Numeric T>       void process(T v) { /* general path  */ }
template<SignedNumeric T> void process(T v) { /* signed-int path */ }

// process(42)    → SignedNumeric overload (subsumes Numeric for signed ints)
// process(42u)   → Numeric overload       (unsigned fails SignedNumeric)
// process(3.14)  → Numeric overload       (float fails SignedNumeric)

Subsumption only holds when concept A is defined as a syntactic conjunction or disjunction of concept B. If you write requires { std::is_integral_v<T>; } instead of requires std::integral<T>, the compiler cannot establish a subsumption relationship and overload resolution becomes ambiguous.

Best Practices

Prefer standard concepts over manually assembled type_traits predicates. std::regular communicates intent in one word; spelling out std::is_default_constructible_v<T> && std::is_copy_constructible_v<T> && std::is_copy_assignable_v<T> && std::is_equality_comparable<T> does not — and loses the subsumption relationship.

Constrain at the declaration, not the body. A static_assert(std::integral<T>) inside a function body fires only during instantiation and provides no benefit to overload resolution. A requires on the template head participates in constraint checking and produces more targeted diagnostics.

Define user concepts in terms of standard concepts, not raw trait values. This preserves subsumption chains that enable fine-grained overload resolution.

Use std::regular as the floor for value-semantic type parameters. Any generic algorithm that copies, compares, or stores values should require at minimum std::regular.

Common Pitfalls

std::regular_invocable carries no compile-time enforcement. It exists to document that a callable is referentially transparent (same inputs → same output, no side effects). The compiler accepts any invocable where regular_invocable is required. Misusing it silently documents incorrect semantics.

Abbreviated templates introduce separate type parameters per auto.

cpp
// f: T1 and T2 are independent — both integral, but may differ
void f(std::integral auto a, std::integral auto b);

// g: single T, both parameters must be the same type
template<std::integral T> void g(T a, T b);

Satisfaction is not subsumption. requires { std::is_integral_v<T>; } and requires std::integral<T> both constrain to integral types, but only the latter participates in the subsumption partial order. Mixing styles breaks overload resolution and produces ambiguity errors.

std::equality_comparable is a semantic contract, not just a syntax check. If operator== exists but is not symmetric, transitive, and reflexive, the concept is satisfied syntactically while the semantic contract is violated. Algorithms that depend on equivalence classes will produce silently incorrect results.

The Arithmetic and Comparison Concept Hierarchy

std::arithmetic unifies integral and floating-point types in a single flat concept, while the signed/unsigned split gives finer-grained overloads. The subsumption chain is: std::signed_integral ⊂ std::integral and std::unsigned_integral ⊂ std::integral; neither of those subsumes std::floating_point; std::arithmetic is not a standard concept — use Numeric as a user concept to achieve the same effect.

cpp
// C++20
#include <concepts>
#include <ranges>
#include <numeric>

// Comparison hierarchy
static_assert(std::totally_ordered<int>);
static_assert(std::totally_ordered<double>);
static_assert(std::three_way_comparable<int, std::strong_ordering>);
static_assert(std::three_way_comparable<double, std::partial_ordering>); // NaN makes it partial

// Arithmetic hierarchy
static_assert(std::integral<int>);
static_assert(std::signed_integral<int>);
static_assert(!std::signed_integral<unsigned int>);
static_assert(std::unsigned_integral<unsigned int>);
static_assert(std::floating_point<double>);
static_assert(!std::integral<double>);

template<typename T>
concept Arithmetic = std::integral<T> || std::floating_point<T>;

The comparison concepts are equally layered: std::equality_comparable requires only ==; std::totally_ordered adds <, >, <=, >=; and std::three_way_comparable<T, Cat> further constrains the category of the spaceship result.

cpp
// C++20
#include <concepts>
#include <ranges>
#include <numeric>

// Practical use: sum over any arithmetic range, preserving element type
template<std::ranges::range R>
    requires Arithmetic<std::ranges::range_value_t<R>>
auto sum(R const& r) {
    using T = std::ranges::range_value_t<R>;
    return std::accumulate(std::ranges::begin(r), std::ranges::end(r), T{});
}

std::ranges::range_value_t<R> extracts the element type, and the Arithmetic constraint ensures that T{} (zero-initialisation) and operator+ are meaningful — rejecting string or pointer ranges at the call site with a readable diagnostic.

std::regular and std::semiregular

std::regular<T> is the C++20 name for a value type: default-constructible, copyable, and equality-comparable. std::semiregular<T> drops only the equality requirement, covering types that can be stored in containers but lack a natural notion of equality.

cpp
// C++20
#include <concepts>
#include <string>
#include <vector>
#include <optional>

// std::regular requires: default_initializable + copyable + equality_comparable
static_assert(std::regular<int>);
static_assert(std::regular<std::string>);
static_assert(std::regular<std::vector<int>>);

// std::optional<T> is semiregular when T is semiregular, regular when T is regular
static_assert(std::semiregular<std::optional<int>>);
static_assert(std::regular<std::optional<int>>);   // int is regular

// A move-only type is neither
struct MoveOnly {
    MoveOnly() = default;
    MoveOnly(MoveOnly&&) noexcept = default;
    MoveOnly& operator=(MoveOnly&&) noexcept = default;
    MoveOnly(const MoveOnly&) = delete;
    MoveOnly& operator=(const MoveOnly&) = delete;
};
static_assert(!std::semiregular<MoveOnly>);   // not copyable

STL containers such as std::vector<T> require T to be at minimum std::movable; std::set<T> additionally requires std::totally_ordered<T>. std::regular is the appropriate constraint for generic algorithm parameters that copy, compare, and store values.

cpp
// C++20
#include <concepts>

// Making a user-defined type satisfy std::regular:
// implement all 5 special members + operator==
struct Point {
    double x{}, y{};

    Point() = default;                                    // default_initializable
    Point(Point const&) = default;                        // copy_constructible
    Point& operator=(Point const&) = default;             // copy_assignable
    Point(Point&&) noexcept = default;                    // move_constructible
    Point& operator=(Point&&) noexcept = default;         // move_assignable

    bool operator==(Point const&) const = default;        // equality_comparable (C++20)
};

static_assert(std::regular<Point>);

Defaulting operator== in C++20 performs member-wise comparison, so a single declaration satisfies std::equality_comparable without any hand-written body.

Invocable Concepts: callable, predicate, relation

std::invocable<F, Args...> requires that std::invoke(f, args...) is a valid expression; std::regular_invocable<F, Args...> adds the semantic constraint that the call is referentially transparent — same inputs always produce the same output without side effects. The compiler enforces the syntactic check but cannot verify the semantic claim.

cpp
// C++20
#include <concepts>
#include <functional>

// std::invocable vs std::regular_invocable
static_assert(std::invocable<decltype([](int x){ return x * 2; }), int>);
static_assert(std::regular_invocable<decltype([](int x){ return x * 2; }), int>);

// std::predicate<F, Args...>: invocable whose return is boolean-testable
template<typename T, std::predicate<T const&> Pred>
bool any_of_range(std::initializer_list<T> items, Pred p) {
    for (auto const& v : items) if (p(v)) return true;
    return false;
}

// std::relation<R, T, U>: binary predicate over T × U
template<typename T, std::relation<T, T> Rel>
bool is_symmetric(T a, T b, Rel r) {
    return r(a, b) == r(b, a);
}

// std::strict_weak_order<R, T>: models a valid sort comparator
template<typename T, std::strict_weak_order<T, T> Cmp>
T const& min_of(T const& a, T const& b, Cmp less) {
    return less(b, a) ? b : a;
}

std::strict_weak_order subsumes std::relation, which subsumes std::predicate, which subsumes std::regular_invocable, which subsumes std::invocable. Constraining a sort comparator to std::strict_weak_order is therefore strictly more constrained than std::invocable and participates correctly in overload resolution.

cpp
// C++20
#include <concepts>
#include <algorithm>
#include <vector>

// Practical generic find API exploiting the predicate hierarchy
template<std::ranges::range R, std::predicate<std::ranges::range_value_t<R>> Pred>
auto find_first(R&& r, Pred p) {
    return std::ranges::find_if(r, p);
}

Writing Your Own Concept Library

Building domain-specific concepts from standard primitives gives you both composability and the subsumption benefit — overloads constrained on your concept automatically subsume its building blocks. The key rule: compose with && and || inside a concept definition, not inside a raw requires expression.

cpp
// C++20
#include <concepts>
#include <iostream>
#include <string>

// --- Numeric: integral or floating-point ---
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

// --- Printable: supports operator<< to std::ostream ---
template<typename T>
concept Printable = requires(T t, std::ostream& os) {
    { os << t } -> std::same_as<std::ostream&>;
};

// --- Serializable: has a .serialize() returning something string-like ---
template<typename T>
concept Serializable = requires(T t) {
    { t.serialize() } -> std::convertible_to<std::string>;
};

// --- Composing with && and || ---
template<typename T>
concept PrintableNumeric = Numeric<T> && Printable<T>;

template<typename T>
concept StorableValue = std::regular<T> && (Printable<T> || Serializable<T>);

Concepts defined with && inside the concept body participate in subsumption: PrintableNumeric subsumes both Numeric and Printable, so an overload constrained on PrintableNumeric is preferred over one constrained on Numeric alone.

cpp
// C++20
#include <concepts>
#include <iostream>
#include <string>

// Concept conjunction enables the most-constrained overload to win
template<Numeric T>
void display(T v) { std::cout << "numeric: " << v << '\n'; }

template<PrintableNumeric T>
void display(T v) { std::cout << "printable numeric: " << v << '\n'; }

// display(42)    → PrintableNumeric overload (int satisfies both; more constrained wins)
// display(3.14f) → PrintableNumeric overload (float satisfies both)

Keep user concepts in a dedicated header, constrain them against static_assert tests, and document which standard concept each one subsumes so callers can reason about overload resolution.

Concept Subsumption and Partial Ordering

Subsumption is the mechanism by which the compiler selects the most constrained viable overload: if concept A's constraint expression is a syntactic conjunction that includes concept B, then A subsumes B, and an overload constrained on A is preferred over one constrained on B. The iterator hierarchy is the clearest real-world example.

cpp
// C++20
#include <concepts>
#include <iterator>

// The standard iterator hierarchy (subsumption shown left-to-right):
// std::input_iterator
//   ⊂ std::forward_iterator
//       ⊂ std::bidirectional_iterator
//           ⊂ std::random_access_iterator
//               ⊂ std::contiguous_iterator

// Three overloads of advance — each subsumes the previous
template<std::input_iterator It>
void advance(It& it, std::iter_difference_t<It> n) {
    while (n-- > 0) ++it;                      // O(n), single-pass
}

template<std::bidirectional_iterator It>
void advance(It& it, std::iter_difference_t<It> n) {
    if (n >= 0) while (n-- > 0) ++it;
    else        while (n++ < 0) --it;          // O(|n|), bidirectional
}

template<std::random_access_iterator It>
void advance(It& it, std::iter_difference_t<It> n) {
    it += n;                                   // O(1), random access
}

When called with a std::vector::iterator, all three overloads are viable; the compiler picks the random_access_iterator overload because it is the most constrained. When called with a std::list::iterator, only the first two are viable, and bidirectional_iterator wins.

cpp
// C++20 — subsumption only works for concepts, NOT raw requires-expressions
#include <concepts>

template<typename T>
    requires (std::integral<T> && sizeof(T) >= 4)   // NOT a concept — no subsumption
void f(T);

template<std::integral T>
void f(T);                                           // ambiguous with the above for int

// Fix: promote the compound constraint to a named concept
template<typename T>
concept WideIntegral = std::integral<T> && sizeof(T) >= 4;

template<WideIntegral T> void g(T);   // subsumes std::integral<T>
template<std::integral T> void g(T);  // less constrained — g(42) picks WideIntegral

The rule is precise: subsumption is established only by atomic concept-id inclusions within a conjunction or disjunction in the concept definition. An inline requires clause with embedded trait checks is opaque to the subsumption algorithm.

Concepts in SFINAE-Legacy Code

Concepts and enable_if express the same intent, but concepts participate in subsumption, yield readable diagnostics, and compose without wrapper metafunctions. Migrating incrementally is safe: concepts and SFINAE overloads coexist in the same overload set.

cpp
// C++20
#include <concepts>
#include <type_traits>

// ── SFINAE style (C++11/14/17) ───────────────────────────────────────────
template<typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
T sfinae_double(T x) { return x * 2; }

// ── Concept style (C++20) ────────────────────────────────────────────────
template<std::integral T>
T concept_double(T x) { return x * 2; }

// Both produce equivalent binary; concept version gives cleaner diagnostics.

Boolean variable templates still work as concept checks inside if constexpr, replacing many tag-dispatch and SFINAE patterns without requiring a separate overload:

cpp
// C++20
#include <concepts>
#include <string>

template<typename T>
std::string describe(T const& v) {
    if constexpr (std::integral<T>)        return "integer";
    else if constexpr (std::floating_point<T>) return "float";
    else if constexpr (std::same_as<T, std::string>) return "string";
    else return "unknown";
}

Migration reference — common SFINAE patterns and their concept equivalents:

SFINAE patternConcept equivalent
enable_if_t<is_integral_v<T>>requires std::integral<T>
enable_if_t<is_base_of_v<B,D>>requires std::derived_from<D, B>
enable_if_t<is_convertible_v<F,T>>requires std::convertible_to<F, T>
enable_if_t<is_copy_constructible_v<T>>requires std::copy_constructible<T>
Void-t detection idiom for ::value_typerequires { typename T::value_type; }
conjunction_v<..., ...>ConceptA<T> && ConceptB<T> in a concept
cpp
// C++20 — concept_of_v boolean variables remain usable wherever a bool is expected
#include <concepts>

// Concept checks as boolean expressions (e.g. in static_assert or if constexpr)
constexpr bool is_int_regular = std::regular<int>;      // true
constexpr bool is_ptr_regular = std::regular<int*>;     // true (pointers are regular)
static_assert(is_int_regular);

When a codebase must stay compatible with C++17 for some translation units, isolate concept-gated APIs in C++20-only headers (#pragma once + module-style includes) and expose C++17-compatible wrappers that forward to them. The ABI is identical; only the constraint machinery differs.

See Also

  • <type_traits> — the pre-C++20 mechanism that most standard concepts wrap
  • <iterator> — iterator concepts: std::input_iterator, std::random_access_iterator, std::sentinel_for
  • <ranges> — range concepts: std::ranges::range, std::ranges::sized_range, std::ranges::viewable_range
  • concept / requires — the C++20 language features that underpin this entire library