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

Constrain Templates with C++20 Concepts and Requires Expressions

Learn to write and compose C++20 concepts and requires expressions to constrain templates, eliminate SFINAE boilerplate, and produce clear error messages.

By the end of this page, you will be able to define named concepts, write requires clauses and requires expressions, compose concepts from simpler building blocks, and diagnose the two most common mistakes people make when constraints appear to pass but shouldn't.


What and Why

Before C++20, constraining a template meant writing std::enable_if guards or arcane void_t tricks. The resulting error messages pointed into the guts of <type_traits>, not at the call site where the programmer made a mistake. Every library worked around this differently, and reading template constraints felt like deciphering assembly.

Concepts are a first-class language feature that lets you name a set of requirements on a type and attach those requirements directly to a template. The compiler checks constraints before instantiation, reports violations at the call site, and uses them for overload resolution. What was once a three-file SFINAE ballet becomes a single readable declaration.

There are two related but distinct syntactic forms:

FormWhat it is
concept Foo = ...;A named, reusable predicate over types or values
requires (T t) { ... }An expression that tests whether a set of operations is valid

A concept uses a requires expression to define what it checks. You can also use requires expressions inline without ever naming a concept.


Step by Step

1. Your first concept

A concept is a bool constant template. The simplest version delegates to a type trait:

cpp
#include <concepts>
#include <type_traits>

// True when T can be copied
template <typename T>
concept Copyable = std::is_copy_constructible_v<T>;

Attach it to a function template using a constrained template parameter:

cpp
template <Copyable T>
T clone(const T& value) {
    return value;
}

struct NoCopy {
    NoCopy(const NoCopy&) = delete;
};

int main() {
    int n = clone(42);        // OK
    // NoCopy x = clone(NoCopy{}); // error: constraint not satisfied
}

The error now names the concept and the failing type β€” not a line inside <type_traits>.

2. The requires expression

A requires expression tests whether a block of statements would be syntactically valid for a given type, without actually executing them. Its result is a bool usable anywhere a constant expression is needed.

cpp
#include <concepts>
#include <string>

// Checks that T has .size() returning something convertible to std::size_t
template <typename T>
concept Sized = requires(T t) {
    { t.size() } -> std::convertible_to<std::size_t>;
};

template <Sized T>
void print_size(const T& container) {
    // guaranteed: container.size() exists and returns something numeric
}

int main() {
    print_size(std::string{"hello"});  // OK
    // print_size(42);                 // error: int doesn't satisfy Sized
}

The braced { expr } -> Concept; form is a compound requirement: it checks both that the expression compiles and that its return type satisfies a follow-up concept.

3. Four kinds of requirements inside a requires expression

cpp
#include <concepts>
#include <iterator>

template <typename T>
concept IterableContainer = requires(T c, typename T::iterator it) {
    // Simple requirement: expression must be valid
    c.begin();
    c.end();

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

    // Compound requirement: expression + return type constraint
    { c.size() } -> std::convertible_to<std::size_t>;

    // Nested requirement: arbitrary boolean predicate
    requires std::forward_iterator<typename T::iterator>;
};

You rarely need all four in one block, but knowing they exist lets you express exactly what your algorithm needs.

4. The requires clause (separate from the expression)

requires appears in two different roles. When it follows a template parameter list or function signature, it is a clause that attaches any bool constant expression as a constraint:

cpp
#include <type_traits>

// Requires clause using a type trait directly
template <typename T>
    requires std::is_arithmetic_v<T>
T square(T x) { return x * x; }

// Requires clause using an inline requires expression
template <typename T>
    requires requires(T a, T b) { a + b; }
T add(T a, T b) { return a + b; }

The double requires requires is legal and common for ad-hoc constraints. The outer requires is the clause; the inner requires begins the expression.


Common Patterns

Pattern 1: Composing concepts with && and ||

Concepts are boolean, so you can combine them with logical operators to build richer abstractions:

cpp
#include <concepts>

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

template <typename T>
concept PrintableNumeric = Numeric<T> && requires(T v) {
    { v } -> std::convertible_to<double>;
};

template <PrintableNumeric T>
double to_double(T v) { return static_cast<double>(v); }

Pattern 2: Constraining a class template

Concepts work on class templates too, gating member functions or the whole specialization:

cpp
#include <concepts>
#include <vector>

template <std::totally_ordered T>
class SortedView {
public:
    explicit SortedView(std::vector<T> data);
    // T is guaranteed to support <, <=, >, >= β€” safe to sort
};

Pattern 3: Abbreviated function templates (C++20)

C++20 lets you write auto in a function parameter list to create an implicit template β€” the same as writing template<typename T> explicitly. When you add a concept name before auto, the parameter is constrained:

cpp
#include <concepts>

// Unconstrained: equivalent to template<typename T> void show(T x)
void show(auto x) {
    std::cout << x << '\n';
}

// Constrained: equivalent to template<std::integral T> T doubled(T x)
std::integral auto doubled(std::integral auto x) {
    return x * 2;
}

// Multiple constrained parameters β€” each gets its own independent T
void swap_print(std::integral auto a, std::integral auto b) {
    std::cout << b << ' ' << a << '\n';
}

// Same type for both parameters requires the explicit template form
template <std::integral T>
void same_type(T a, T b) { }

Abbreviated templates are concise for single-parameter utilities, but use the explicit template<typename T> form when you need to refer to the type T elsewhere β€” in the return type, another parameter, or a local variable.

Pattern 4: Concept-based overloading

cpp
#include <concepts>
#include <iterator>
#include <numeric>

// Fast path: random-access iterators allow cheap distance
template <std::random_access_iterator It>
auto fast_sum(It first, It last) {
    return std::accumulate(first, last, 0);
}

// Fallback: any input iterator
template <std::input_iterator It>
auto fast_sum(It first, It last) {
    typename std::iterator_traits<It>::value_type acc{};
    for (; first != last; ++first) acc += *first;
    return acc;
}

The compiler automatically selects the specialization that best matches the iterator category of the argument.

Debugging Constraints

When a concept check fails at the call site the error message names the violated concept, but sometimes you want to verify conformance proactively β€” before shipping a type or at the top of a file β€” rather than waiting for an accidental misuse to trigger the diagnostic.

Use static_assert with the concept as a boolean predicate. The optional message string gives future readers (and your future self) an immediate explanation:

cpp
#include <concepts>

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

struct MyVec2 { float x, y; };
MyVec2 operator+(MyVec2 a, MyVec2 b) { return {a.x + b.x, a.y + b.y}; }

// Verify at definition time β€” fails loudly if the type drifts
static_assert(Summable<MyVec2>, "MyVec2 must satisfy Summable");
static_assert(Summable<int>);
static_assert(!Summable<std::string>);  // string + string yields string, not ill-formed
                                        // so this will actually fail β€” see note below

You can also build a small concept_check.cpp translation unit that does nothing but assert your public types against the concepts your library exports. Compilation of that file becomes a contract test that runs on every build:

cpp
// concept_check.cpp β€” compile with -std=c++20; no main() needed
#include "mylib/vec2.hpp"
#include "mylib/concepts.hpp"

static_assert(Summable<MyVec2>,     "MyVec2 regression: Summable broken");
static_assert(Copyable<MyVec2>,     "MyVec2 regression: Copyable broken");
static_assert(!Summable<RawBuffer>, "RawBuffer must NOT satisfy Summable");

Note that static_assert(ConceptName<T>) and a requires-clause serve different purposes. The requires-clause is a constraint β€” it participates in overload resolution and SFINAE. static_assert is an assertion that fires unconditionally if the condition is false. Use requires-clauses to express what a template demands from its callers; use static_assert to express invariants you want confirmed at a specific point in translation.

Standard Range Concepts

The <ranges> header ships a hierarchy of concepts under std::ranges that categorize any type based on what its iterator can do. Each level refines the one above it: satisfying a stronger concept automatically satisfies all weaker ones.

cpp
std::ranges::range
    └── std::ranges::input_range
            └── std::ranges::forward_range
                    └── std::ranges::bidirectional_range
                            └── std::ranges::random_access_range
                                    └── std::ranges::contiguous_range
ConceptWhat the iterator supportsExample types
std::ranges::rangebegin() and end() β€” basic iterationany iterable
std::ranges::input_rangesingle-pass readstd::istream_iterator ranges
std::ranges::forward_rangemulti-pass read, multiple live iteratorsstd::forward_list
std::ranges::bidirectional_rangeforward and backward traversalstd::list, std::set
std::ranges::random_access_rangeO(1) index access via []std::deque, std::vector
std::ranges::contiguous_rangeelements contiguous in memory; pointer arithmetic validstd::array, std::vector, std::string

Two orthogonal concepts layer on top of the hierarchy:

  • std::ranges::sized_range β€” std::ranges::size(r) runs in amortized O(1). Most containers qualify; a linked list built with a sentinel end does not.
  • std::ranges::common_range β€” begin() and end() return the same type. Standard containers satisfy this; many views do not (their sentinel is a distinct type).

Use these concepts to constrain algorithms that need specific capabilities:

cpp
#include <ranges>
#include <vector>
#include <list>

// Only accepts ranges where elements are stored contiguously in memory
template <std::ranges::contiguous_range R>
void write_to_socket(const R& data) {
    // safe to pass data(data) + size(data) to a C API
    auto* ptr = std::ranges::data(data);
    auto  len = std::ranges::size(data);
    // ... send(fd, ptr, len, 0);
}

void example() {
    std::vector<char> buf = {'h','e','l','l','o'};
    write_to_socket(buf);          // OK: vector is contiguous_range

    std::list<char> lst = {'h','i'};
    // write_to_socket(lst);       // error: list is not contiguous_range
}

Constraining Lambdas (C++20)

C++20 extended lambdas to support explicit template parameter lists, and those template parameters can carry a requires-clause just like any other function template:

cpp
#include <concepts>
#include <vector>
#include <list>

// Template lambda with a requires clause
auto double_it = []<typename T> requires std::integral<T>(T x) {
    return x * 2;
};

int  a = double_it(21);      // OK β€” int satisfies std::integral
// double_it(3.14);          // error: double does not satisfy std::integral

The abbreviated form β€” placing the concept name before auto in the parameter list β€” is more concise for simple cases:

cpp
// Equivalent abbreviated form
auto double_it_short = [](std::integral auto x) {
    return x * 2;
};

Constrained lambdas are useful when you need to pass a heterogeneous callable to a higher-order function and want the constraint to be checked at the call site inside the algorithm, not deferred until a template deep in the library fails:

cpp
#include <concepts>
#include <algorithm>
#include <vector>

// A generic algorithm that applies a projection to each element
template <std::ranges::input_range R, typename Proj>
void transform_in_place(R& range, Proj proj) {
    for (auto& elem : range) {
        elem = proj(elem);
    }
}

void example() {
    std::vector<int> v = {1, 2, 3, 4};

    // Constrained lambda: ensures the projection works on integers
    transform_in_place(v, []<typename T> requires std::integral<T>(T x) {
        return x * x;
    });
    // v is now {1, 4, 9, 16}
}

When a lambda carries both an explicit template parameter list and a requires-clause, the requires keyword appears after the closing > of the template list and before the parameter list (...).


What Can Go Wrong

Mistake 1: Confusing syntactic validity with semantic correctness

A requires expression checks whether an expression compiles, not whether it does the right thing. This concept appears to require addition, but will accept types where + does something unexpected:

cpp
template <typename T>
concept Addable = requires(T a, T b) {
    a + b;  // only checks that + is valid syntax
};

A type that overloads + to return void satisfies Addable. Add a compound requirement to constrain the return type:

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

Mistake 2: Overlapping concepts causing ambiguous overloads

Two concepts that both match a type create ambiguity unless one subsumes the other. Subsumption only happens when one concept is defined in terms of the other (or uses && to include it):

cpp
#include <concepts>

template <typename T>
concept A = std::integral<T>;

template <typename T>
concept B = std::integral<T> && (sizeof(T) >= 4);  // B subsumes A

template <A T> void f(T);   // less constrained
template <B T> void f(T);   // more constrained β€” wins for int, long

// If A and B were independent boolean formulas with no subsumption
// relationship, calling f(42) would be ambiguous.

When writing pairs of overloads, always establish subsumption explicitly.


Quick Reference

SyntaxPurpose
template <ConceptName T>Constrain T using a named concept
void f(ConceptName auto x)Abbreviated function template β€” C++20
requires BoolExpr (clause)Attach any bool constant as a constraint
requires (T t) { expr; }Requires expression: simple requirement
requires (T t) { { expr } -> Concept; }Compound: expression + return-type constraint
requires (T t) { typename T::nested; }Type requirement: nested type must exist
requires (T t) { requires Pred<T>; }Nested requirement: nested boolean predicate
concept Foo = A<T> && B<T>;Concept composition (subsumption applies)
concept Foo = A<T> || B<T>;Either constraint is sufficient

Standard concepts in <concepts>:
std::same_as, std::convertible_to, std::derived_from, std::constructible_from, std::integral, std::signed_integral, std::floating_point, std::totally_ordered, std::equality_comparable, std::invocable, std::regular, std::copyable, std::movable, and more. Iterator concepts live in <iterator>: std::input_iterator, std::forward_iterator, std::bidirectional_iterator, std::random_access_iterator, std::contiguous_iterator.


What's Next