Concepts (C++20)
C++20 concepts are named, composable boolean predicates on template parameters that replace SFINAE with readable constraints and precise error messages.
Conceptssince C++20A concept is a named boolean predicate evaluated at compile time that constrains template parameters, enabling readable constraint expressions, clean overload resolution, and informative error messages in place of SFINAE substitution failures.
Overview
Before C++20, constraining template parameters meant either documentation-only (hoping callers read it) or SFINAE β a mechanism so hostile to human readers that entire libraries existed solely to make it writable. Concepts replace both with a first-class language feature.
A concept names a constraint. The compiler checks it before instantiation, so violations produce a message that names the unsatisfied concept rather than a wall of substitution failures. Concepts also participate in overload resolution through subsumption: the most-constrained viable overload is preferred, without hacks like tag dispatch or enable_if chains.
Concepts interact closely with three other C++20 features: requires clauses (attach constraints to existing templates), requires expressions (test whether operations are syntactically valid), and abbreviated function templates (void f(Concept auto x)). All four are distinct constructs that compose.
Syntax
Defining a concept
// Concept definition β C++20
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>; // C++20
// requires expression inside a concept
template<typename T>
concept Container = requires(T c) {
{ c.begin() } -> std::input_iterator; // compound requirement
{ c.end() } -> std::input_iterator;
{ c.size() } -> std::convertible_to<size_t>;
typename T::value_type; // type requirement
};
// Combining concepts with && and ||
template<typename T>
concept SortableContainer = Container<T>
&& std::sortable<std::ranges::iterator_t<T>>; // C++20 rangesFour ways to apply a concept
// 1. Constrained template parameter β most common
template<std::integral T>
void print_bits(T value);
// 2. requires clause β use when the constraint is complex or conditional
template<typename T>
requires std::integral<T> && (sizeof(T) >= 4)
void print_bits(T value);
// 3. Abbreviated function template β C++20 shorthand, equivalent to (1)
void print_bits(std::integral auto value);
// 4. Constrained auto in variable or return type
std::integral auto result = compute_id(); // deduced type must satisfy std::integralAll four are semantically equivalent for simple cases. Prefer (1) when the parameter name is needed elsewhere in the signature; (3) for one-liner lambdas and short functions.
Requires expression anatomy
A requires expression tests syntactic validity β it is itself a bool constant expression usable anywhere a constant expression is expected.
// requires expression β C++20
requires(T a, T b) {
// Simple requirement: expression must be well-formed
a + b;
// Type requirement: nested type must exist
typename T::value_type;
// Compound requirement: expression valid AND return type satisfies concept
{ a + b } -> std::same_as<T>;
{ a.size() } -> std::convertible_to<std::size_t>;
// noexcept requirement: expression must be declared noexcept
{ a.swap(b) } noexcept;
// Compound + noexcept together
{ a.clone() } noexcept -> std::same_as<T>;
};The -> in a compound requirement takes a concept, not a type. { expr } -> std::same_as<int> checks that the expression's type satisfies same_as<int>, which means the type is exactly int. Use std::convertible_to<int> to accept anything implicitly convertible.
Examples
Overloading without enable_if
Concepts enable clean, ordered overloads. The compiler picks the most-constrained matching overload β subsumption β without explicit priority tricks.
#include <concepts>
#include <print>
// C++20: overloads ranked by constraint specificity
template<std::integral T>
void describe(T x) { std::println("integral: {}", x); } // C++23 std::println
template<std::floating_point T>
void describe(T x) { std::println("floating: {}", x); }
template<std::arithmetic T> // integral || floating_point
void describe(T x) { std::println("arithmetic: {}", x); } // never reached β subsumed
template<typename T>
void describe(T x) { std::println("other"); }
// describe(42) -> "integral: 42"
// describe(3.14) -> "floating: 3.14"
// describe("hi") -> "other"std::arithmetic<T> is subsumed by both std::integral<T> and std::floating_point<T>, so it is never the most-constrained match when either of those applies.
Writing a real concept: Hashable key
#include <concepts>
#include <functional>
template<typename K>
concept HashMapKey =
std::regular<K> // copyable, equality-comparable, default-constructible
&& requires(K k) {
{ std::hash<K>{}(k) } noexcept -> std::convertible_to<std::size_t>;
};
template<HashMapKey K, typename V>
class FlatMap { /* ... */ };
FlatMap<std::string, int> table1; // OK
FlatMap<std::vector<int>, int> t2; // error: vector has no std::hash specializationRecursive and dependent concepts
#include <concepts>
#include <ranges>
// Concept constraining a range whose elements themselves satisfy a concept
template<typename R, typename C>
concept RangeOf = std::ranges::range<R>
&& C<std::ranges::range_value_t<R>>;
// Use: accept ranges of integral values only
template<typename R>
requires RangeOf<R, std::integral>
auto sum(R&& range) {
std::ranges::range_value_t<R> total{};
for (auto v : range) total += v;
return total;
}Concept-constrained lambdas (C++20)
auto square = []<std::arithmetic T>(T x) { return x * x; };
// Or with abbreviated syntax:
auto square = [](std::arithmetic auto x) { return x * x; };Best Practices
Name concepts at the right granularity. A concept like HasSize that only checks c.size() is less useful than std::ranges::sized_range, which captures the full semantic contract. Favour reusing standard concepts and composing them over writing one-off structural checks.
Prefer std::same_as over raw type checks in compound requirements. { expr } -> std::same_as<T> is explicit about exact type. If you write { a + b } with no -> annotation, the return type is unconstrained β the requirement only checks that a + b compiles.
Use requires clauses for multi-parameter constraints. When a constraint spans multiple template parameters or involves relationships between them, a requires clause is clearer than cramming everything into individual parameter constraints.
// Relationship constraint β requires clause is the right tool
template<typename From, typename To>
requires std::convertible_to<From, To> && (!std::same_as<From, To>)
To convert(From x);Prefer naming a concept over inlining a requires expression in a clause. Unnamed requires requires double-keywords are valid but obscure β name the concept instead so the constraint is reusable and self-documenting.
Common Pitfalls
Concepts verify syntax, not semantics
A concept satisfied by structural checking cannot enforce the semantic contract. A type can have operator+ that violates mathematical associativity and still satisfy Addable. Concepts narrow down plausible types; correctness remains the programmer's responsibility.
template<typename T>
concept Addable = requires(T a, T b) { { a + b } -> std::same_as<T>; };
struct Broken {
Broken operator+(Broken) const { return {}; } // always returns default
};
static_assert(Addable<Broken>); // passes β syntax onlyThe double requires is syntactically valid but rarely wanted
// valid but confusing β outer requires clause, inner requires expression
template<typename T>
requires requires(T t) { t.foo(); }
void f(T t);
// better β name it
template<typename T>
concept HasFoo = requires(T t) { t.foo(); };
template<HasFoo T>
void f(T t);Subsumption only works through concept definitions, not raw expressions
The compiler can only subsume constraints it can decompose into named atomic concepts. Two structurally identical requires expressions in separate clauses are not subsumed β they are treated as distinct, unordered constraints, causing ambiguity.
// WRONG β ambiguous, no subsumption between raw requires expressions
template<typename T>
requires requires(T t) { t.begin(); }
void g(T t);
template<typename T>
requires requires(T t) { t.begin(); } && requires(T t) { t.size(); }
void g(T t); // NOT more constrained than the above β ambiguous call
// RIGHT β use named concepts so subsumption can apply
template<std::ranges::range T>
void g(T t);
template<std::ranges::sized_range T> // sized_range subsumes range
void g(T t); // unambiguously more constrained-> ConceptName takes a unary concept, not a type
{ expr } -> int is a compile error. The -> syntax requires a concept: { expr } -> std::same_as<int> or { expr } -> std::convertible_to<int>.
Concepts vs SFINAE: Side-by-Side
The same constraint expressed three ways reveals why concepts won. SFINAE with enable_if_t compiles but is nearly unreadable; tag dispatch is readable but verbose; concepts are both readable and participate in real overload resolution through subsumption.
// C++11 β SFINAE with enable_if_t
template<typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
void process(T value) { /* integral path */ }
template<typename T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
void process(T value) { /* floating-point path */ }
// Error message on bad call: "no matching function" + wall of substitution failures// C++17 β tag dispatch, cleaner but still indirect
namespace detail {
template<typename T> void process_impl(T v, std::true_type) { /* integral */ }
template<typename T> void process_impl(T v, std::false_type) { /* float */ }
}
template<typename T>
void process(T v) { detail::process_impl(v, std::is_integral<T>{}); }
// Error message on bad call: points to the detail:: layer, not the constraint// C++20 β concept constraints
template<std::integral T>
void process(T value) { /* integral path */ }
template<std::floating_point T>
void process(T value) { /* floating-point path */ }
// Error message on bad call: "constraints not satisfied: std::integral<std::string>"Unlike SFINAE, which uses first-match ordering, concepts participate in subsumption: the compiler selects the most-constrained viable overload. A concept B subsumes concept A when B's definition expands to include A as an atomic constraint, so std::signed_integral subsumes std::integral without any priority tricks.
Standard Concepts Quick Reference
The <concepts> header provides the building blocks most generic code needs. Iterators and ranges add their own concepts in <iterator> and <ranges> respectively.
| Category | Concept | Meaning |
|---|---|---|
| Type properties | std::integral<T> | integer type (incl. bool, char) |
std::floating_point<T> | float, double, long double | |
std::signed_integral<T> | signed integer type | |
std::unsigned_integral<T> | unsigned integer type | |
std::arithmetic<T> | integral or floating_point | |
| Object | std::movable<T> | move-constructible and move-assignable |
std::copyable<T> | copyable and movable | |
std::semiregular<T> | copyable and default-constructible | |
std::regular<T> | semiregular and equality-comparable | |
| Comparison | std::equality_comparable<T> | supports == and != |
std::totally_ordered<T> | supports all six comparison operators | |
std::three_way_comparable<T> | supports <=> (spaceship) | |
| Callable | std::invocable<F, Args...> | callable with given argument types |
std::predicate<F, Args...> | invocable returning bool-convertible | |
std::relation<R, T, U> | binary predicate (strict weak ordering) |
// C++20 β using standard concepts from <concepts>
#include <concepts>
template<std::regular T>
class Registry {
std::vector<T> entries_;
public:
void insert(T val) { entries_.push_back(std::move(val)); }
bool contains(const T& val) const {
return std::ranges::find(entries_, val) != entries_.end();
}
};
// std::regular guarantees: default-constructible, copyable, equality-comparable
// β everything Registry needs without listing each constraint manuallyConcept Satisfaction and Diagnostics
Concept diagnostics name the failing clause; SFINAE diagnostics name the symptom. When a concept check fails, the compiler walks the constraint tree and reports exactly which atomic requirement was not met, rather than printing the full substitution context.
// C++20 β deliberately failing a concept check
#include <concepts>
template<std::totally_ordered T>
T clamp(T val, T lo, T hi) { return std::clamp(val, lo, hi); }
struct Opaque { int x; }; // no operator< defined
// clamp(Opaque{3}, Opaque{1}, Opaque{5});
// GCC output:
// error: use of function 'clamp(T, T, T) [with T = Opaque]'
// note: constraints not satisfied
// note: 'std::totally_ordered<Opaque>' evaluated to false
// note: because 'Opaque' does not satisfy 'std::three_way_comparable'
// note: because '<=> *expr*' would be ill-formed
//
// Clang output:
// error: no matching function for call to 'clamp'
// note: candidate template ignored: constraints not satisfied [with T = Opaque]
// note: because 'Opaque' does not satisfy 'totally_ordered'The double-requires shorthand (requires requires(T t) { ... }) inlines an anonymous concept directly in a clause. It is syntactically valid and sometimes handy for one-off checks, but the constraint cannot be subsumed or reused elsewhere.
// C++20 β double-requires (ad-hoc, non-reusable)
template<typename T>
requires requires(T t) { t.serialize(); t.deserialize(); }
void round_trip(T& obj);
// Prefer naming it when the constraint appears more than once:
template<typename T>
concept Serializable = requires(T t) { t.serialize(); t.deserialize(); };
template<Serializable T>
void round_trip(T& obj);The named-concept form also enables subsumption and produces better error messages that reference Serializable by name.
Concepts with Multiple Template Parameters
Multi-parameter concepts express relationships between types, not just properties of one type. Standard examples include std::assignable_from<T, U>, std::convertible_to<From, To>, and std::common_with<T, U>.
// C++20 β two-parameter concept
template<typename T, typename U>
concept AssignableFrom = requires(T t, U u) {
{ t = u } -> std::same_as<T&>;
};
// Multi-parameter concept used in a function template
template<typename From, typename To>
requires AssignableFrom<To, From>
void assign(To& dest, From src) { dest = src; }// C++20 β sentinel concept (from the standard library design)
template<typename I, typename S>
concept SentinelFor =
std::semiregular<S>
&& std::input_or_output_iterator<I>
&& std::__WeaklyEqualityComparableWith<I, S>; // internal detail shown for illustration
// Multi-parameter concepts cannot be used with abbreviated function template syntax:
// void f(AssignableFrom auto x, ??? auto y) <- ill-formed; no place for second param
// Use a requires clause instead:
template<typename T, typename U>
requires AssignableFrom<T, U>
void f(T& dest, U src);Two-parameter (or N-parameter) concepts must be applied via a requires clause or a constrained template parameter list; the abbreviated Concept auto syntax only works for single-type concepts.
Concept-Constrained Class Templates
Concepts constrain class templates the same way they constrain function templates. The constraint is checked at the point of instantiation, and violations name the concept rather than producing a cascade of member-function errors.
// C++20 β concept-constrained class template
#include <concepts>
template<std::integral T>
class Counter {
T value_{};
public:
Counter() = default;
explicit Counter(T init) : value_(init) {}
Counter& operator++() { ++value_; return *this; }
Counter operator++(int) { auto tmp = *this; ++value_; return tmp; }
T get() const noexcept { return value_; }
// Constrained member function β accepts any type convertible to T
template<typename U>
requires std::convertible_to<U, T>
void add(U val) { value_ += static_cast<T>(val); }
};
Counter<int> ci; // OK
Counter<double> cd; // error: 'double' does not satisfy 'std::integral'// C++20 β partial specialization with concept constraint
template<typename T>
class Storage { /* general case */ };
template<std::trivially_copyable T>
class Storage<T> {
// Specialisation can use memcpy and stack buffers safely
alignas(T) std::byte buf_[sizeof(T)];
public:
void store(const T& v) { std::memcpy(buf_, &v, sizeof(T)); }
T load() const { T v; std::memcpy(&v, buf_, sizeof(T)); return v; }
};Only the constrained parameters need a concept; a class template with three parameters can constrain just one, leaving the others unconstrained.
Concepts in std::ranges Algorithms
The ranges library is built entirely on concepts, so every algorithm documents its requirements as named concepts from <iterator> and <ranges> rather than informal prose.
// C++20 β ranges algorithms rely on concepts for static checking
#include <algorithm>
#include <ranges>
#include <vector>
std::vector<int> nums = {3, 1, 4, 1, 5, 9, 2, 6};
// std::ranges::sort requires std::sortable<I> β random-access iterator +
// indirectly_swappable + indirect_strict_weak_order
std::ranges::sort(nums); // C++20
// std::ranges::find_if requires std::indirect_unary_predicate<Pred, I>
auto it = std::ranges::find_if(nums, [](int n){ return n > 4; }); // C++20// C++20 β projection parameter in ranges algorithms
#include <algorithm>
#include <ranges>
#include <string>
#include <vector>
struct Person { std::string name; int age; };
std::vector<Person> people = {{"Alice", 30}, {"Bob", 25}, {"Carol", 35}};
// Sort by age using a projection β {} is a transparent comparator (std::ranges::less{})
// &Person::age is the projection applied to each element before comparison
std::ranges::sort(people, {}, &Person::age); // C++20
// find_if with projection
auto senior = std::ranges::find_if(people, [](int a){ return a >= 30; }, &Person::age);The projection parameter transforms each element before the comparator or predicate sees it, cleanly separating the "how to compare" from the "what field to compare". Ranges algorithms also propagate concept errors up-front: calling std::ranges::sort on a std::list fails immediately with a std::sortable / random-access-iterator constraint message rather than deep inside the sort implementation.
See Also
<concepts>β standard concept definitions- SFINAE and
enable_ifβ what concepts replace - Ranges β C++20 ranges library built on concepts
- Abbreviated function templates β
autoas a constrained parameter if constexprβ compile-time branching, often used alongside concepts