Skip to content
C++

Defining Named Concepts

An inline requires expression is useful, but writing the same constraint in multiple templates is error-prone and opaque to readers. A named concept packages a constraint into a reusable, self-documenting name. A concept name in a template parameter list reads like a type qualifier — it tells you immediately what the type must be capable of, without requiring the reader to inspect a requires expression buried in the implementation.

The concept keyword

A concept is defined using the concept keyword, template syntax, and a compile-time boolean expression on the right-hand side of =. That boolean can be a type trait variable template, a requires expression, a conjunction or disjunction of existing concepts, or any combination. The result is always a named predicate over the template parameters.

// Simplest form: concept wrapping a type trait
template<typename T>
concept Arithmetic = std::is_arithmetic_v<T>;

// Equivalent using a requires expression
template<typename T>
concept Arithmetic = requires { std::is_arithmetic_v<T>; };  // simple requirement

// Richer form: concept with a requires expression body
template<typename T>
concept Addable = requires(T a, T b) {
    a + b;          // simple requirement: + must be valid
};

// Multi-parameter concept
template<typename T, typename U>
concept ConvertibleTo = std::is_convertible_v<T, U>;

A concept evaluates to a compile-time boolean, but naming conventions should reflect that it describes a quality or role, not a predicate. Prefer adjective or noun names — Arithmetic, Copyable, Container, Logger — over verb-style names like IsArithmetic or HasBegin. The standard library follows this convention: std::integral, std::copyable, std::range.

Four syntaxes for applying a concept

Once defined, a concept can be applied to template parameters in four equivalent ways. All four express the same constraint; choose the form that reads most naturally in context. The abbreviated function template form (using auto with a concept prefix) is the most concise and is covered in depth in the next lesson.

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

// 1. Concept name replaces 'typename' in the template parameter list
template<Arithmetic T>
T add(T a, T b) { return a + b; }

// 2. Concept in a requires clause (after the template parameter list)
template<typename T>
requires Arithmetic<T>
T add(T a, T b) { return a + b; }

// 3. Concept in a trailing requires clause (after the function declaration)
template<typename T>
T add(T a, T b) requires Arithmetic<T>
{ return a + b; }

// 4. Abbreviated function template — covered in the next lesson
auto add(Arithmetic auto a, Arithmetic auto b) { return a + b; }

// All four are equivalent and accept only arithmetic types:
add(1, 2);          // OK
add(1.0, 2.0);      // OK
// add("a", "b");   // error: const char* is not Arithmetic

Replacing a SFINAE type trait with a concept

Before C++20, checking structural properties of a type — "does it have a begin() member? does it have a nested value_type?" — required a combination of std::void_t, partial specialization, and helper variable templates. The resulting type trait was many lines of boilerplate, hard to read, and produced terrible error messages when unsatisfied. A concept achieves the same check in a fraction of the code and produces a diagnostic that explains exactly which sub-requirement failed.

// Pre-C++20: is_container type trait (simplified)
template<typename T, typename = void>
struct is_container : std::false_type {};

template<typename T>
struct is_container<T,
    std::void_t<
        typename T::value_type, typename T::iterator,
        decltype(std::declval<T>().begin()),
        decltype(std::declval<T>().size())>> : std::true_type {};

// 15 lines. Produces cryptic error messages. Requires a helper variable template.

// C++20 concept: equivalent in 9 lines, reads like a contract
template<typename T>
concept Container = requires(T c) {
    typename T::value_type;
    typename T::iterator;
    { c.begin() } -> std::same_as<typename T::iterator>;
    { c.size()  } -> std::convertible_to<std::size_t>;
};

// Usage is identical in both cases:
template<Container C>
void process(C& c) { /* ... */ }

Concepts can also be used with static_assert directly, since they evaluate to a compile-time boolean. This is useful for enforcing preconditions in library code or for writing tests that verify concept satisfaction.

static_assert(Container<std::vector<int>>);
static_assert(Container<std::list<std::string>>);
static_assert(!Container<int>);
static_assert(!Container<std::pair<int,int>>);  // no iterator, no size()

Composing concepts with && and ||

Concepts compose with the && and || operators, forming conjunctions and disjunctions. Both are short-circuit evaluated during concept checking: for a conjunction, if the left operand is false the right one is not checked; for a disjunction, if the left operand is true the right one is skipped. This matters because checking a concept can itself trigger substitution failures, so ordering and short-circuiting affect compilation correctness.

// Building a hierarchy of concepts with &&
template<typename T>
concept Integral = std::is_integral_v<T>;

template<typename T>
concept Signed = std::is_signed_v<T>;

// Conjunction: T must satisfy BOTH Integral and Signed
template<typename T>
concept SignedIntegral = Integral<T> && Signed<T>;

template<SignedIntegral T>
T abs_val(T v) { return v < 0 ? -v : v; }

abs_val(-42);          // OK: int is signed integral
// abs_val(42u);       // error: unsigned is Integral but not Signed
// abs_val(3.14);      // error: double is Signed but not Integral
// Disjunction: T must satisfy AT LEAST ONE of the concepts
template<typename T>
concept FloatingPoint = std::is_floating_point_v<T>;

template<typename T>
concept Number = Integral<T> || FloatingPoint<T>;

template<Number T>
T add(T a, T b) { return a + b; }

add(1, 2);        // OK: int is Integral
add(1.5, 2.5);    // OK: double is FloatingPoint
// add("a","b");  // error: not a Number

Concept subsumption: more constrained wins

When overload resolution encounters two viable template functions that differ only in their constraints, the compiler must pick the more constrained one. This process is called subsumption. A concept C1 subsumes another concept C2 when C1's atomic constraints form a superset of C2's — informally, when satisfying C1 necessarily means satisfying C2. The compiler tracks this by normalising concepts into conjunctions and disjunctions of atomic constraints.

A critical implication: this subsumption analysis only works when constraints are expressed as named concepts, not as raw type traits. Two type trait expressions that happen to express the same thing are treated as unrelated atomic constraints, producing an ambiguity error. Replacing type traits with concepts enables the compiler to see the relationship.

template<typename T>
concept Integral = std::is_integral_v<T>;

// Overload 1: any Integral type
template<Integral T>
T add(T a, T b) { return a + b; }

// Overload 2: Integral AND 4 bytes — more constrained, subsumes overload 1
template<Integral T>
requires (sizeof(T) == 4)
T add(T a, T b) { return a + b; }

add((short)1, (short)2);  // only overload 1 matches (short is 2 bytes)
add(1, 2);                // both match; overload 2 is more constrained — selected
// No ambiguity because the compiler sees Integral<T> in both declarations
// and knows overload 2 subsumes overload 1.

// ✗ CONTRAST: raw type traits — no subsumption, ambiguity error
template<typename T>
requires std::is_integral_v<T>
T add_bad(T a, T b) { return a + b; }

template<typename T>
requires std::is_integral_v<T> && (sizeof(T) == 4)
T add_bad(T a, T b) { return a + b; }

// add_bad(1, 2); // ERROR: ambiguous — compiler cannot see that one subsumes the other

Constraining class templates and member functions

Concepts apply to class templates in the same way as to function templates — place the concept name instead of typename in the parameter list, or add a requires clause. Individual non-template member functions of a class template can also carry their own requires clauses, so a member function only exists when the class's type parameter satisfies a specific extra requirement.

// Class template constrained to integral types only
template<std::integral T>
struct numeric_buffer {
    T data[64];

    T sum() const {
        T acc = 0;
        for (auto v : data) acc += v;
        return acc;
    }

    // member constrained further: division only makes sense for signed types
    T average() const requires std::signed_integral<T> {
        return sum() / 64;
    }
};

numeric_buffer<int>      bi;   // OK
// numeric_buffer<float>  bf;  // error: float is not integral

bi.sum();       // OK
bi.average();   // OK: int is signed_integral

numeric_buffer<unsigned> bu;
bu.sum();       // OK
// bu.average(); // error: unsigned is not signed_integral

Constraining members on top of the class constraint is preferable to the pre-C++20 SFINAE approach of converting the member to a template and using enable_if — the result is shorter, the intent is clear from the declaration, and the error messages name the unsatisfied concept directly.

The standard concepts library <concepts>

The C++20 standard library provides a rich set of predefined concepts in the <concepts> header. Using them avoids redefining common predicates from scratch and enables proper subsumption — the standard concepts are wired into the ordering rules that govern overload resolution. The library concepts are grouped into several categories.

Core language / type categories

std::integral, std::floating_point, std::signed_integral, std::unsigned_integral, std::same_as<T,U>, std::derived_from<D,B>

Convertibility and common types

std::convertible_to<From,To>, std::common_with<T,U>, std::common_reference_with<T,U>

Object concepts

std::destructible, std::constructible_from<T,Args...>, std::default_initializable, std::move_constructible, std::copy_constructible

Comparison concepts

std::equality_comparable, std::totally_ordered, std::three_way_comparable

Callable concepts

std::invocable<F,Args...>, std::regular_invocable<F,Args...>, std::predicate<F,Args...>

Iterator and range concepts (in <iterator> / <ranges>)

std::input_iterator, std::forward_iterator, std::random_access_iterator, std::range, std::sized_range
#include <concepts>

// Using standard concepts directly in your own templates
template<std::totally_ordered T>
T clamp(T val, T lo, T hi) {
    return val < lo ? lo : val > hi ? hi : val;
}

// Composing with standard concepts
template<typename T>
concept Printable = requires(T t) {
    { std::cout << t } -> std::same_as<std::ostream&>;
};

template<Printable T>
requires std::equality_comparable<T>
void print_if_unique(const std::vector<T>& v) {
    for (std::size_t i = 0; i < v.size(); ++i) {
        bool unique = true;
        for (std::size_t j = 0; j < v.size(); ++j)
            if (i != j && v[i] == v[j]) { unique = false; break; }
        if (unique) std::cout << v[i] << ' ';
    }
}
Sign in to track progress