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>.
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