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: 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:
#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
#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:
#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 belowYou 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:
// 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.
std::ranges::range
βββ std::ranges::input_range
βββ std::ranges::forward_range
βββ std::ranges::bidirectional_range
βββ std::ranges::random_access_range
βββ std::ranges::contiguous_range| Concept | What the iterator supports | Example types |
|---|---|---|
std::ranges::range | begin() and end() β basic iteration | any iterable |
std::ranges::input_range | single-pass read | std::istream_iterator ranges |
std::ranges::forward_range | multi-pass read, multiple live iterators | std::forward_list |
std::ranges::bidirectional_range | forward and backward traversal | std::list, std::set |
std::ranges::random_access_range | O(1) index access via [] | std::deque, std::vector |
std::ranges::contiguous_range | elements contiguous in memory; pointer arithmetic valid | std::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()andend()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:
#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:
#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::integralThe abbreviated form β placing the concept name before auto in the parameter list β is more concise for simple cases:
// 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:
#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:
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 |
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
- 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