Chapter 14: Concepts and Constraints
Concepts let you describe the expectations that a template places on its arguments. Instead of waiting for a deep instantiation error, you give the compiler a clear statement of intent, and it checks constraints before instantiating. This leads to better diagnostics, clearer interfaces, and more robust generic code. This chapter explores the purpose of constraints, the forms of requires, how to craft custom concepts, how to use constrained auto, and how to move from older SFINAE techniques to modern expressions.
Why constraints improve diagnostics
Without constraints, templates accept almost anything until substitution reaches a point where something fails to compile. The resulting error may refer to a detail buried far inside another template. A constraint moves that check to the boundary of the template so that failure happens early and the message points directly to the violated assumption.
Failing early at the call site
When a constraint is not satisfied, the compiler does not instantiate the template at all. This puts responsibility on the caller rather than the internals, which helps pinpoint the exact mismatch. Constraints therefore serve both as documentation and enforcement.
template <class T>
requires std::integral<T>
T half(T x) { return x / 2; }
half(3.0); // error at the call site; double is not integral
Using requires
A requires clause attaches a constraint to a declaration. A requires expression checks whether a set of operations or properties is valid for a given set of types or values. Both are central to expressing intent about template parameters.
Requires clausess
Use a requires clause after the template parameter list or after the function signature to state requirements on arguments. Clauses can combine concepts with logical operators such as && and || for more expressive contracts.
template <class T>
requires std::copy_constructible<T> && std::movable<T>
void swap_em(T& a, T& b) {
T tmp = a;
a = b;
b = tmp;
}
Requires expressions
A requires expression describes exact operations that must be valid in a context. It can check expression validity, return types, and nested constraints while remaining purely compile time logic.
template <class T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::same_as<T>;
{ a += b };
};
Defining custom concepts
Concepts let you name recurring patterns and use them in many templates. Custom concepts turn informal requirements into reusable constraints that improve readability and maintainability.
Concept definitions
A concept is a compile time predicate that returns true or false. You can compose concepts with logical operators, and you can combine broad concepts with narrow ones to refine meaning.
template <class T>
concept NumberLike =
std::integral<T> ||
std::floating_point<T>;
template <NumberLike T>
T scale(T x, T factor) { return x * factor; }
Traits and concepts working together
Use type traits to query properties such as nested typedefs, constant expressions, or member presence, then wrap those checks in a concept. This preserves the benefits of existing trait infrastructure while giving you clear constraint boundaries.
template <class T>
concept HasValueType = requires { typename T::value_type; };
Constrained auto and abbreviated templates
C++20 introduced constrained auto which allows a concept to appear directly in parameter lists. This is known as an abbreviated template. It reduces verbosity by placing constraints exactly where parameters appear rather than at the top of a declaration.
Using concepts with auto
You can place a concept before auto to constrain a parameter inline. The compiler treats this as shorthand for a template declaration with the concept as a constraint.
void print_twice(std::integral auto x) {
std::cout << x << " " << x;
}
Abbreviated templates
Abbreviated templates reduce noise in simple function templates. They also help reveal intent at the point of use. Keep this style for shorter functions and reserve fuller template forms for complex contracts.
std::totally_ordered auto max3(std::totally_ordered auto a,
std::totally_ordered auto b,
std::totally_ordered auto c) {
return std::max(std::max(a, b), c);
}
Migrating SFINAE to concepts
Older template code used SFINAE to remove overloads from consideration when substitution failed. Concepts make this intent explicit and readable. Migrating from SFINAE to concepts usually simplifies code, reveals intent, and trims dense helper machinery.
Replacing enable_if patterns with constraints
Patterns such as std::enable_if_t<cond, T> are replaced with straightforward requires clauses or concept constrained parameters. This removes boilerplate and puts the condition where people expect to find it.
// old style
template <class T,
class = std::enable_if_t<std::integral<T>::value>>
T f(T x);
// modern style
template <std::integral T>
T f(T x);
Trait based detection
Type trait detection used to chain specializations, void_t patterns, and fallback overloads. Concepts now express the desired property directly using requires expressions and combine naturally with the rest of the type system.
// old detection idiom
template <class, class = void>
struct HasSize : std::false_type {};
template <class T>
struct HasSize<T, std::void_t<decltype(std::declval<T>().size())>>
: std::true_type {};
// modern concept
template <class T>
concept HasSizeC = requires(T t) { t.size(); };