Forwarding references (also known as universal references) provide the ability to write a template that can deduce and accept any
kind of reference to the object (rvalue/lvalue mutable/const). This enables the creation of a perfect forwarding
constructor for wrapper types: the constructor arguments are forwarded to build the underlying type:
class Wrapper {
public:
// A defaulted copy constructor
Wrapper(Wrapper const& other) = default;
template <typename T>
Wrapper(T&& str) // A noncompliant forwarding constructor
: str(std::forward<T>(str)) {}
private:
std::string str;
};
However, this constructor is too greedy: overload resolution prefers it over the copy constructor as soon as the argument type is slightly
different from a Wrapper const&
. For instance, when passing a non-const lvalue (w
in the following example),
calling the copy constructor requires a non-const to const conversion, while the forwarding reference parameter is an exact match, and will therefore
be selected. This is usually not the expected behavior.
Wrapper const cw("str1");
Wrapper w("str2");
Wrapper w1(cw); // Ok: calls Wrapper(Wrapper const& other)
Wrapper w2(w); // Ill-formed: calls Wrapper(T&& str) with [T = Wrapper&]
// This tries to initialize a std::string using a Wrapper object
This rule specifically targets constructors that can be called with a single forwarding reference argument. In such cases, they compete
with copy or move constructors, including those implicitly generated by the compiler. Yet, selecting the wrong overload can also happen with
forwarding references on regular functions and methods, but this is out of scope for this rule.
Even if the non-constrained forwarding constructor may currently seem to work fine, using it with different value categories in the future could
result in unexpected compilation errors or, even worse, hard-to-debug run-time behavior if the wrapped type happens to be constructible from instances
of the wrapper.