Why is this an issue?
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, which allows a wrapper to be constructed with any type that the underlying type can be constructed with:
template<typename T>
class Wrapper {
public:
Wrapper() = default;
Wrapper(const Wrapper& other) : value(other.value) {}
template<typename U>
Wrapper(U&& u) // forwarding constructor
: value(std::forward<U>(u))
{}
private:
T value;
};
However, this constructor is too greedy: It will be preferred by overload resolution over the copy constructor as soon as the argument type is
slightly different from a const Wrapper&
. For instance, when passing a non-const lvalue, calling the copy constructor
requires a benign non-const to const conversion, while the forwarding reference parameter is an exact match, and will therefore be selected. This is
usually not the correct behaviour.
Wrapper<int> w;
Wrapper<int> const cw;
Wrapper<int> w2(cw); // ok, calls Wrapper(Wrapper const& other)
Wrapper<int> w3(w); // ill-formed, calls Wrapper(U&& u) with U = Wrapper<int>&
The greediness of forwarding references is a general problem, but it is even stronger for constructors that can be called with a single argument of
forwarding reference type (including variadic template constructors), because in that case they compete with copy or move constructors.
To eliminate this pitfall, such constructors should be constrained in a way that it is not considered a candidate when U&&
is
a reference to the class type or to a class derived from the class type. In the previous example, this can be achieved by adding to the forwarding
reference constructor:
- A check of the concept
!same_as<remove_cvref_t<U>, Wrapper>
- A check of type predicate
!is_same_v<remove_cvref_t<U>, Wrapper>
, or
- An
enable_if
with the equivalent condition.
If Wrapper
were a base class, those checks would become:
- The concept
derived_from<std::remove_cvref_t<U>, Wrapper>
- A type-predicate
!is_base_of_v<Wrapper, std::remove_cvref_t<U>>
, or
- An
enable_if
with the equivalent condition.
The concept-based solutions require C++20; while the enable_if
solution, which is more cumbersome to write, can always be used.
This rule raises an issue when a class has a template constructor that can be called with a single argument bound to a forwarding reference
parameter, and that constructor is not constrained in one of the previously mentioned ways. There are other ways to ensure that such a constructor
cannot be used for objects of the same type, but this rule only detects explicit checks.
Noncompliant code example
template<typename T>
class Wrapper {
public:
Wrapper(Wrapper const& other) : value(other.value) {}
template<typename U>
Wrapper(U&& u) // noncompliant
: value(std::forward<U>(u))
{}
private:
T value;
};
template<typename T>
class OtherWrapper {
public:
OtherWrapper(OtherWrapper const& other) = default;
template<typename U>
requires std::constructible_from<T, U>
OtherWrapper(U&& u) // noncompliant, constructible_from check is not sufficient in general
: value(std::forward<U>(u))
{}
private:
T value;
};
template<typename T>
class EmplaceWrapper {
public:
EmplaceWrapper(EmplaceWrapper const& other) = default;
template<typename... Args>
requires std::constructible_from<T, Args...>
EmplaceWrapper(Args&&... args) // noncompliant, will compete with copy-constructor
: value(std::forward<Args>(args)...)
{}
private:
T value;
};
Compliant solution
template<typename T>
class WrapperCpp20 {
public:
WrapperCpp20(WrapperCpp20 const& other) : value(other.value) {}
template<typename U>
requires (!std::same_as<WrapperCpp20, std::remove_cvref_t<U>>)
WrapperCpp20(U&& u)
: value(std::forward<U>(u))
{}
private:
T value;
};
template<typename T>
class WrapperCpp11 {
public:
WrapperCpp11(WrapperCpp11 const& other) : value(other.value) {}
template<typename U,
typename std::enable_if<
!std::is_same<
WrapperCpp11,
typename std::remove_cv<typename std::remove_reference<U>::type>::type
>::value,
int>::type /* Unnamed */ = 0>
WrapperCpp11(U&& u)
: value(std::forward<U>(u))
{}
private:
T value;
};
template<typename T>
class OtherWrapper {
public:
OtherWrapper(OtherWrapper const& other) : value(other.value) {}
template<typename U>
requires (!std::derived_from<std::remove_cvref_t<U>, OtherWrapper>) && std::constructible_from<T, U>
OtherWrapper(U&& u)
: value(std::forward<U>(u))
{}
private:
T value;
};
template<typename T>
class EmplaceWrapper {
public:
EmplaceWrapper(EmplaceWrapper const& other) = default;
template<typename... Args>
requires std::constructible_from<T, Args...>
EmplaceWrapper(std::in_place_t, Args&&... args) // compliant, no longer competes with copy-constructor
: value(std::forward<Args>(args)...)
{}
private:
T value;
};
Resources
- Effective Modern C++ item 29: Avoid overloading on universal references