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:
| Form | What 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:
#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:
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.
#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
#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:
#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:
#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:
#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: Concept-based overloading
Overload resolution prefers the more constrained candidate, letting you replace if constexpr chains with separate functions:
#include <concepts>
#include <iterator>
#include <numeric>
// Fast path: random-access iterators allow O(1) 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.
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:
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:
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):
#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
| Syntax | Purpose |
|---|---|
template <ConceptName T> | Constrain T using a named concept |
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 |
Concepts you already have in <concepts>:
std::same_as, std::convertible_to, std::derived_from, std::integral, std::floating_point, std::totally_ordered, std::invocable, std::regular, and more.
What's Next
- Advanced concept techniques β partial specialization with concepts, abbreviated function templates, and
autoparameters - The requires expression in depth β all four requirement forms with edge cases
- constexpr if (advanced) β when
if constexpris still the right tool alongside concepts - Concepts β full language reference for the concept declaration syntax