Concepts
Concepts (C++20) are named, reusable constraints on template parameters. A concept is a compile-time predicate over a type or a set of types; it can appear in template parameter lists, in requires-clauses, in the auto keyword to constrain otherwise-unconstrained type deduction, and in if constexpr branches. The mechanism is the standard’s response to the long-standing problems of template error-message complexity (a typo in a generic call could produce hundreds of lines of unintelligible diagnostics) and the awkward SFINAE idioms that pre-C++20 generic code relied on. Concepts make constraints explicit, reusable, and diagnosable.
The motivation
Pre-C++20 templates accept any type that, in the body, is valid for the operations the template performs. The constraint is implicit: the template body uses <, +, member functions, etc., and the substitution succeeds or fails on those operations.
template <typename T>
T sum(const std::vector<T> &v) {
T total = T{};
for (const auto &x : v) total = total + x;
return total;
}
sum<MyType> works if MyType is default-constructible and supports operator+. Any type for which the body fails to substitute produces a compilation error — typically a long, deeply-nested message about which expression in which template instantiation failed.
Concepts make the constraint explicit:
template <typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>;
T{};
};
template <Addable T>
T sum(const std::vector<T> &v) {
T total = T{};
for (const auto &x : v) total = total + x;
return total;
}
The constraint Addable is checked at the call site, not deep inside the template body. A type that does not satisfy Addable produces a diagnostic naming Addable and the missing operation directly.
Defining concepts
A concept is defined with concept:
template <typename T>
concept Numeric = std::is_arithmetic_v<T>;
template <typename T>
concept Comparable = requires(T a, T b) {
{ a == b } -> std::convertible_to<bool>;
{ a != b } -> std::convertible_to<bool>;
{ a < b } -> std::convertible_to<bool>;
{ a > b } -> std::convertible_to<bool>;
};
The body of a concept is a constraint expression: a compile-time boolean. The simplest forms use type traits (std::is_arithmetic_v<T>); more elaborate forms use requires-expressions to test for the presence of operations.
requires-expressions
A requires-expression evaluates to true if every requirement inside is satisfied for the named types:
template <typename T>
concept Drawable = requires(T t, std::ostream &os) {
t.draw(os); // expression must compile
{ t.size() } -> std::convertible_to<int>; // expression compiles, type constraint
typename T::shape_kind; // T has a nested type
requires sizeof(T) <= 256; // arbitrary boolean
};
The four kinds of requirement:
| Form | Meaning |
|---|---|
expr; | The expression must be valid (compile). |
{ expr } -> Constraint; | The expression must be valid, and its type must satisfy Constraint. |
typename T::name; | T::name must name a type. |
requires expr; | The (compile-time boolean) expression must be true. |
The expressions inside the requires-block are not evaluated; only their well-formedness is checked.
requires-clauses
A requires-clause attaches a constraint to a template:
template <typename T>
T sum(const std::vector<T> &v) requires Addable<T> {
/* … */
}
template <typename T> requires Numeric<T>
T square(T x) { return x * x; }
Two equivalent placements: at the end of the function (after the parameter list) or before the function declaration. The first form is more common for the trailing constraints; the second is more common for parameter-related constraints.
Concepts as type-parameter constraints
The most concise form replaces typename in the parameter list:
template <Addable T>
T sum(const std::vector<T> &v) { /* … */ }
Addable T is shorthand for typename T requires Addable<T>. The form is the conventional choice when a single concept constrains a parameter.
For multiple constraints, the requires-clause is more readable:
template <typename T>
requires Numeric<T> && Comparable<T>
T pick_smaller(T a, T b);
Concepts in the auto keyword
A concept may constrain auto:
auto first(const std::vector<int> &v) -> Numeric auto {
return v.front();
}
void process(Drawable auto &d) {
d.draw(std::cout);
}
auto x = std::vector<int>{}; // unconstrained
Drawable auto d = make_widget(); // constrained
The Drawable auto form is the constrained-auto shorthand: it permits any type that satisfies Drawable. Function parameters declared with constrained auto introduce abbreviated function templates — the compiler synthesises the template parameter list:
void process(Drawable auto &d);
// equivalent to:
template <Drawable T>
void process(T &d);
The construction is the conventional way to declare simple generic functions in modern code.
Standard concepts
The <concepts> header defines a set of standard concepts:
| Concept | Tests |
|---|---|
std::same_as<T, U> | T and U are the same type |
std::convertible_to<T, U> | T is convertible to U |
std::derived_from<D, B> | D derives from B |
std::integral<T> | T is an integer type |
std::signed_integral<T> | T is a signed integer type |
std::floating_point<T> | T is a floating-point type |
std::default_initializable<T> | T may be default-initialised |
std::copy_constructible<T> | T may be copy-constructed |
std::move_constructible<T> | T may be move-constructed |
std::equality_comparable<T> | T supports == and != |
std::totally_ordered<T> | T supports <, <=, >, >=, ==, != |
std::invocable<F, Args...> | F may be called with Args |
std::predicate<F, Args...> | F returns a bool-convertible value |
std::regular<T> | T is regular (default-constructible, copyable, equality-comparable) |
std::semiregular<T> | T is regular but without the equality requirement |
Additional concepts cover iterators (std::input_iterator, std::random_access_iterator), ranges, and the standard algorithms’ constraints.
Concept-based overload resolution
Concepts integrate with overload resolution: when two overloads differ in their constraints, the more constrained one is selected when both apply. The “more constrained” relation is defined by subsumption: a concept A subsumes B if A’s constraint expression implies B’s.
template <typename T>
void f(T x) {
/* generic */
}
template <std::integral T>
void f(T x) {
/* integer-specialised */
}
f(3); // calls the integral overload (more constrained)
f(3.14); // calls the generic overload (only one viable)
The mechanism replaces the enable_if/SFINAE patterns of pre-C++20 code, where the same effect required substantially more boilerplate.
The relationship to SFINAE
SFINAE — Substitution Failure Is Not An Error — was the pre-C++20 mechanism for constraint-based overload selection. The pattern:
template <typename T,
typename = std::enable_if_t<std::is_integral_v<T>>>
void f(T x);
The same constraint, with concepts:
template <std::integral T>
void f(T x);
The concepts version is shorter, produces better diagnostics, and integrates cleanly with the rest of the language. SFINAE remains legal — large bodies of pre-C++20 code use it — but new code should use concepts where they apply.
The cases where SFINAE remains useful are narrow: extremely fine-grained selection that the concept system cannot easily express, or libraries that must support pre-C++20 compilers. For typical generic code, concepts are the correct mechanism.
Idiomatic use
The conventional patterns:
Constrain the public interface, not the implementation
template <std::ranges::range R>
auto sum(R &&r) {
typename std::ranges::range_value_t<R> total{};
for (auto &&x : r) total = total + x;
return total;
}
The constraint std::ranges::range R is at the parameter level; the implementation can use auto and let the compiler propagate the type.
Compose concepts
template <typename T>
concept Container = std::ranges::range<T> &&
requires { typename T::value_type; };
template <typename T>
concept Sortable = std::random_access_iterator<typename T::iterator> &&
std::totally_ordered<typename T::value_type>;
Concepts are first-class predicates; conjunction (&&) and disjunction (||) compose them.
Constrain auto for clarity
auto compute(std::ranges::input_range auto &&data) {
return std::ranges::distance(data);
}
The constraint on auto documents the function’s contract more clearly than an unconstrained generic parameter.
Use standard concepts before defining new ones
The <concepts>, <iterator>, and <ranges> headers cover most of the conventional cases. New concepts should be defined for genuine domain-specific constraints, not for things the standard library already names.
A note on adoption
Concepts require C++20. Compiler support is mature (GCC, Clang, MSVC all support them by 2022). Library support is similarly broad: the standard library’s containers, iterators, and ranges are constrained with concepts as of C++20. New code targeting C++20 or later should use concepts as the primary constraint mechanism; SFINAE should be reserved for pre-C++20 code or for the rare cases that concepts cannot easily express.