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.

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