Since C++20, it is possible to add a requires-clause to a template as a way to express requirements (constraints) on the template
arguments. This construct is versatile and allows any expression that evaluates to either true
or false
at compile time to
be used. One of these expressions is the requires-expression, which can be used to express required operations on types:
template<typename M>
requires requires(M a, M b) {
{ a + b };
{ a * b };
}
M fma(M a, M b, M c) {
return a + (b * c);
}
While the use of an ad-hoc requires-expression provides a way to quickly constrain a template, it limits the readability of the code:
While it conveys the required syntax requirements, it fails to express the semantics associated with the operations. For instance, the above template
expects that +
and *
perform mathematical additions and multiplications, not a concatenation of strings.
Using a concept with a well-chosen name solves this issue and meaningful concepts that are reused in different parts of the codebase increase the
level of abstraction of the code: You don’t need to check individual operations, you just need to know that your type is integral.
Moreover, one interesting feature of concepts is called subsumption. It means that when a function overload is constrained with a list of
concepts, and another overload is constrained with the same concepts plus additional constraints, then the second function is considered as more
constrained than the first one, and will be selected by overload resolution if the concepts are satisfied.
This feature only works with concepts: a requires-expression is never considered to be more constrained than another one, even if both
contain identical subexpressions. For instance, the following overloads of rotate
are ambiguous for any iterator that is at least
bidirectional, as constraints for both of them are satisfied:
template<typename ForwardIt>
requires requires(ForwardIt it) {
/* dereference and others */
++it;
}
ForwardIt rotate(ForwardIt first, ForwardIt mid, ForwardIt last);
template<typename BidirectionalIt>
requires requires(BidirectionalIt it) {
/* dereference and others */
++it;
--it;
}
BidirectionalIt rotate(BidirectionalIt first, BidirectionalIt mid, BidirectionalIt last);
This rule raises an issue for any use of an ad-hoc requires-expression in the requirements of template functions, classes, or
variables.
Noncompliant code example
template<typename M>
requires requires(M a, M b) { // noncompliant
{ a + b };
{ a * b };
}
M fma(M a, M b, M c) {
return a + (b * c);
}
template<typename ForwardIt>
requires requires(ForwardIt it) { // noncompliant
/* dereference and others */
++it;
}
ForwardIt rotate(ForwardIt first, ForwardIt mid, ForwardIt last);
template<typename BidirectionalIt>
requires requires(BidirectionalIt it) { // noncompliant
/* dereference and others */
++it;
--it;
}
BidirectionalIt rotate(BidirectionalIt first, BidirectionalIt mid, BidirectionalIt last);
Compliant solution
template<typename T>
concept Multiplicative = requires(const T a, const T b) {
{ a + b };
{ a * b };
}
template<Multiplicative M>
M fma(M a, M b, M c) {
return a + (b * c);
}
template<class ForwardIt>
requires std::forward_iterator<ForwardIt>
ForwardIt rotate(ForwardIt first, ForwardIt mid, ForwardIt last);
// std::bidirectional_iterator subsumes std::forward_iterator, as it is defined as:
// template<class I>
// concept bidirectional_iterator = forward_iterator<I> && additional requirements;
template<class BidirectionalIt>
requires std::bidirectional_iterator<BidirectionalIt>
BidirectionalIt rotate(BidirectionalIt first, BidirectionalIt mid, BidirectionalIt last);