C++20 concepts let a template declaration state compiler-checkable requirements at its interface. A constrained candidate participates in overload resolution only when its associated constraints are satisfied. Constraints therefore replace many std::enable_if SFINAE uses with declarations that expose the intended operation set; they do not prove the semantic requirements documented by an API.
Declaring Constraints #
A concept is a namespace-scope, variable-like template whose definition is a bool constant expression:
#include <concepts>
template <class T>
concept Addable = requires(T value) {
{ value + value } -> std::convertible_to<T>;
};Apply a concept with a requires-clause, a constrained template parameter, or an abbreviated function template:
template <class T>
requires Addable<T>
T sum(T left, T right) {
return left + right;
}
std::integral auto increment(std::integral auto value) {
return value + 1;
}Each std::integral auto introduces a distinct template parameter. Use an explicit parameter when two arguments must have the same deduced type. A trailing requires-clause is useful when the constraint follows the declarator, including for a member of a class template:
template <class T>
struct Buffer {
void compact() requires std::movable<T>;
};A requires-clause cannot appear on a non-template free function. Constraints from multiple declaration positions combine with &&; put a prerequisite before a requirement whose substitution depends on that prerequisite.
Requires-Expressions and Semantic Requirements #
A requires-expression checks requirements without evaluating its operands. It supports simple requirements, type requirements, compound requirements, and nested requirements:
#include <concepts>
#include <iterator>
template <class T>
concept SortableRange = requires(T range) {
typename T::value_type; // type
{ range.begin() } -> std::input_or_output_iterator; // compound
range.end(); // simple
requires std::movable<typename T::value_type>; // nested
};An invalid requirement after substitution makes a dependent constraint unsatisfied rather than producing a diagnostic at the call site. A requirement invalid for every possible substitution is ill-formed, no diagnostic required.
Satisfaction is syntactic. A type can satisfy an expression requirement for equality yet violate the equivalence relation expected by std::equality_comparable; a type can make swap compile yet violate its stated complexity or postconditions. Document and test these semantic requirements because the compiler cannot establish them.
Migrating From enable_if
#
Migrate the existing selection policy, rather than broadening the overload set merely because a concept is available:
// Before C++20.
template <class T, std::enable_if_t<std::integral<T>, int> = 0>
T twice(T value) { return value + value; }
// C++20, with the same intended eligibility.
template <std::integral T>
T twice(T value) { return value + value; }Constraints select candidate viability; they do not turn errors in a selected function body into substitution failures. Preserve exclusion cases, conversions, and ordering tests when replacing enable_if, especially where an old overload used a defaulted template parameter or return-type SFINAE.
Ordering and Subsumption #
When several constrained overloads are satisfied, the language uses constraint subsumption to select a more constrained candidate. Subsumption is structural, based on normalized atomic constraints, not arbitrary Boolean equivalence. Name shared predicates and compose them:
template <class T>
concept Range = requires(T value) { value.begin(); value.end(); };
template <class T>
concept SizedRange = Range<T> && requires(T value) { value.size(); };
template <Range T>
void inspect(const T&);
template <SizedRange T>
void inspect(const T&); // selected when both are satisfied
Repeating expressions that happen to mean the same thing can create distinct atomic constraints and make overloads ambiguous. A named concept preserves the atomic constraint identity needed by the ordering rules.
Forwarding References and Customization Points #
For T&& where T is deduced, an lvalue argument deduces T as an lvalue-reference type. Constrain std::remove_cvref_t<T> when the contract concerns the underlying object:
#include <ranges>
#include <type_traits>
template <class R>
requires std::ranges::range<std::remove_cvref_t<R>>
void consume(R&& range);Constrain R directly only when reference qualification is intentional. This distinction prevents an lvalue from being rejected merely because a concept was written for object types.
A customization point object (CPO) is commonly an inline constexpr function object whose constrained call operator implements a defined customization protocol. For example, std::ranges::begin can obtain an iterator from an array, a suitable member begin, or an unqualified begin found by argument-dependent lookup. Constraints should describe the supported protocol at the CPO boundary so unsupported types fail at that interface rather than deep in a selected customization.
Comments
Read or comment on GitHub (sign-in required to comment)