Type-constraints provide a concise way to express constraints on the type deduced for a given template parameter or auto placeholder. In a
situation when a type-constraint is applied to a forwarding reference parameter (T&&
), the corresponding concept will be checked
against the lvalue reference (if the argument is an lvalue) or the plain type (if the argument is an rvalue):
template <SomeConcept T> void func(T&& x);
void f() {
func(SomeType{}); // Argument is an rvalue -> T is deduced as 'SomeType'
SomeType obj;
func(obj); // Argument is lvalue -> T is deduced as 'SomeType&'
}
Even if it is possible to write SomeConcept
in a way that works for both plain types and references, it is not straightforward and
requires a dedicated effort.
Many standard-provided constraints change their behavior when the type is a reference. For instance, a std::copyable
constraint is
never satisfied for references, regardless of the referenced type, while a std::copy_constructible
constraint always is. The following
example illustrates this:
template <std::copyable T> void func(T&& t); // Overload #1.
template <typename T> void func(T&& t); // Overload #2 (unconstrained).
void f() {
// Call with an rvalue argument:
func(std::string{""}); // Calls #1: T is 'std::string', which satisfies 'std::copyable'.
// Call with an lvalue argument:
std::string s{""};
func(s); // Calls #2: T is a reference type ('std::string&') and therefore does not satisfy 'std::copyable'.
}
The rule raises an issue when a forwarding reference parameter is constrained by a standard-provided concept using type-constraint
syntax.
Exceptions
The rule does not raise an issue for the concepts std::convertible_to
and std::ranges::range
with its refinements (like
std::ranges::forward_range
, std::ranges::bidirectional_range
), that handle forwarding reference parameters correctly.