Concepts: the constraint language
Why concepts
Templates allow algorithms to work with any type that satisfies a set of requirements. Before C++20 those requirements were expressed with SFINAE tricks such as std::enable_if and trait metafunctions. The constraints were hidden inside long type expressions, and a failure produced a cascade of template‑instantiation diagnostics that were difficult to read. Concepts replace that style with explicit, named predicates. A concept is part of the function signature. The compiler checks it before it attempts to instantiate the template. If the requirement is not met, the diagnostic cites the concept name and the offending type. The error becomes clear and the intent obvious.
Concepts also act as in‑code documentation: the concept name (e.g., std::integral or std::range) states the precondition next to the declaration, removing the need for separate enable_if blocks. A concept is a compile‑time bool value, usable in if constexpr or static_assert, and it drives overload resolution and in‑body branching. Because the compiler checks the constraint before template instantiation, failures appear at the call site with the concept name and offending type, providing earlier, clearer diagnostics than a static_assert inside the function body.
The requires clause and requires expression
A requires clause follows a template declaration and names one or more concepts that must be satisfied.
template<std::integral T>
void f(T t) {
std::cout << "integral: " << t << '\n';
}
A requires expression appears inside a template body and describes the operations that must exist for a particular set of types.
template<typename T>
requires requires (T a) { { a + a } -> std::same_as<T>; }
T twice(T a) { return a + a; }
If the predicate evaluates to false, the overload is removed from overload resolution. The following example demonstrates two overloads, one for integral types and one for floating‑point types, and prints which overload ran.
#include <iostream>
#include <type_traits>
// Overload for integral types
template<std::integral T>
void show(T value) {
(void)value;
std::cout << "integral overload\n";
}
// Overload for floating‑point types
template<std::floating_point T>
void show(T value) {
(void)value;
std::cout << "floating overload\n";
}
int main() {
show(42); // integral
show(3.14); // floating
return 0;
}
The requires expression can be combined with logical operators to form richer constraints:
template<typename T>
requires (std::integral<T> && sizeof(T) >= 4)
void process(T value) {
std::cout << "'integral (sizeof >= 4)'} " << value << '\n';
}
The compiler evaluates the whole Boolean expression before overload resolution, eliminating surprising matches.
The requires clause and the requires expression are distinct tools. The clause (requires std::integral<T>) attaches a constraint to a declaration. The expression (the requires (T a) { ... } block) describes the operations a type must support and can test return types with -> std::same_as<U>. A requires expression can also guard a single member function, so a class template can offer an operation only when the element type supports it.
Defining a concept
A concept is a named predicate. It can be written with the concept keyword and a requires clause that enumerates the required expressions.
#include <iostream>
#include <type_traits>
// Concept that requires addition
template<typename T>
concept Addable = requires (T a, T b) { a + b; };
// Function using the concept
template<Addable T>
T add(T a, T b) {
return a + b;
}
int main() {
std::cout << "int add: " << add(2, 3) << std::endl;
std::cout << "string add: " << add(std::string{"hi"}, std::string{"!"}) << std::endl;
return 0;
}
The Addable concept checks that the binary + operator is valid for two operands of type T. The accompanying add function uses the concept as a constraint, so any type that models Addable can be added safely. Because the concept name appears directly in the signature, the intent is clear at the call site.
Concepts can also be expressed as Boolean formulas of other concepts. This enables a small vocabulary of high‑level requirements while reusing primitive concepts from <concepts>:
template<typename T>
concept Number = std::integral<T> || std::floating_point<T>;
A library author can build more expressive constraints by layering concepts.
A requires expression enumerates several checks separated by semicolons inside the brace, each a statement that must be valid. Return-type constraints use an arrow: { a + b } -> std::same_as<T> demands that a + b not only compiles but also yields a type convertible to T. Because a concept reduces to a constexpr bool, you can combine concepts with &&, ||, and ! to build richer requirements without writing a new named concept.
The same Boolean nature lets a function branch on a concept with if constexpr, selecting an implementation without writing separate overloads. if constexpr (std::is_pointer_v<T>) { /* pointer path */ } else { /* value path */ } picks the branch at compile time and discards the unused one, so the body need not be valid for every T. This replaces the old tag-dispatch and SFINAE patterns for in-body selection.
Subsumption
When two concepts overlap, the more specific one wins. This rule is called subsumption and mirrors goal ordering in Prolog: the most specific rule is chosen before a more generic one. The compiler builds a partial ordering of candidate functions based on the subsumption relationship of their constraints.
#include <iostream>
#include <type_traits>
// Overload for integral types
template<std::integral T>
void foo(T) {
std::cout << "integral overload" << std::endl;
}
// Overload for signed integral types (more specific)
template<std::signed_integral T>
void foo(T) {
std::cout << "signed integral overload" << std::endl;
}
int main() {
unsigned int u = 1;
int s = -1;
foo(u); // should select integral overload
foo(s); // should select signed integral overload
return 0;
}
Subsumption removes ambiguity from overload sets. When two constrained overloads both match, the compiler prefers the overload whose constraint subsumes the other. For example, std::signed_integral implies std::integral. Calls with a signed type select the std::signed_integral overload, while unsigned calls select the generic overload. This deterministic ordering eliminates the ambiguous‑overload errors common with SFINAE tricks.
Terse syntax
C++26 allows constraints to appear directly on a parameter type or on a plain auto placeholder.
void show(std::integral auto x) { std::cout << x << '\n'; }
std::integral auto n = 5;
The same pattern works for references, forwarding references, and ranges. The example below uses a constrained variable, a constrained parameter, and a call to std::ranges::sort.
#include <iostream>
#include <vector>
#include <algorithm>
#include <ranges>
// Constrained parameter
void show(std::integral auto n) {
std::cout << "value: " << n << '\n';
}
int main() {
std::integral auto n = 7; // constrained variable
show(n);
std::vector<int> v = {5, 2, 9, 1, 4};
std::ranges::sort(v);
std::cout << "sorted:";
for (int x : v) std::cout << ' ' << x;
std::cout << std::endl;
return 0;
}
These terse declarations keep the constraint next to the entity it describes, improving readability. Because the constraint travels with the parameter, a constrained lambda passed to std::ranges::sort is checked the moment the call is written, not when the algorithm instantiates. They also work inside generic lambdas. This enables concise constraint specifications:
auto cmp = [] (std::totally_ordered auto const& a, std::totally_ordered auto const& b) {
return a < b;
};
Constraints also attach to return types through the same auto syntax: std::integral auto square(std::integral auto x) { return x * x } constrains both the parameter and the result. Algorithm authors lean on std::predicate and std::relation to describe the callables they accept, so a sorting routine declares std::predicate<std::weak_ordering, T, T> instead of a vague template parameter.
Constrained algorithms and ranges
Standard algorithms already carry concept requirements. std::ranges::sort requires std::sortable. std::ranges::find requires std::range and an std::indirectly_comparable predicate. The compiler checks those concepts before instantiating the algorithm, so a call that does not meet the requirements fails at the call site with a clear diagnostic.
#include <iostream>
#include <vector>
#include <algorithm>
#include <ranges>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
auto it = std::ranges::find(v, 3);
if (it != v.end()) {
std::cout << "found" << std::endl;
} else {
std::cout << "not found" << std::endl;
}
return 0;
}
The program searches a std::vector<int> for a value. Because the container satisfies std::range, the call compiles. A raw array fails the range concept, and the compiler reports that the concept is not satisfied. Since the concepts appear in the algorithm’s signature, users see the preconditions directly without consulting external documentation or static assertions.
Library concept vocabulary
The header <concepts> provides the core building blocks used throughout the standard library:
std::integral: all integral types.std::floating_point: all floating‑point types.std::same_as<T, U>: two types are identical.std::convertible_to<From, To>: implicit conversion is possible.std::invocable<F, Args...>: a callable can be invoked with the given arguments.std::range<R>: a type providesbeginandenditerators.
The iterator library adds concepts such as std::input_iterator, std::forward_iterator, std::random_access_iterator, and std::contiguous_iterator. The ranges library refines those with concepts like std::sized_range and std::view. These names become part of the public interface. Reading an algorithm signature tells you exactly which properties the arguments must provide.
Several concepts describe callables rather than types. std::predicate<P, Args...> is true when P can be called on Args... and the result is convertible to bool, which is exactly what std::ranges::find_if requires of its comparator. std::relation and std::strict_weak_order capture the ordering contracts that sorting and set operations depend on. Using them in your own signatures lets the compiler reject a comparator that returns the wrong category before any element is compared.
The std::predicate and std::relation concepts are exactly the constraints behind the algorithms from chapters 12 and 13, so the range pipelines written there were concept-checked whether or not the signatures made it explicit.
Concepts are the modern replacement for the SFINAE machinery that chapter 21 teaches as reading fluency. Where an old signature used std::enable_if in a return type, a new signature writes the concept in the parameter list. The behaviour is equivalent, but the new form is checkable before instantiation and readable at a glance.
Reading concept diagnostics
When a constraint fails, the compiler prints the name of the concept and the type that caused the failure. For example, compiling the following code:
template<std::integral T>
void g(T) {}
g("text");
produces a diagnostic similar to:
error: static assertion failed: constraints not satisfied required from ‘std::integral<char const*>’ note: substitution failed for ‘T = char const*’ The message shows that
std::integralwas the offending concept and thatchar const*does not satisfy it. This is far clearer than the cascades of template‑instantiation errors that appeared before concepts.
When user‑defined concepts are involved, the diagnostic includes the failed sub‑requirement. This makes it possible to pinpoint exactly which operation is missing. For instance, if Addable were violated because + returned the wrong type, the compiler points to the { a + b } -> std::same_as<T> clause.
Try this
Define a concept Comparable that requires both a < b and a == b to be valid. Then write a min function constrained by Comparable. Call the function with two int values and with two std::string values.