287 lines
10 KiB
Plaintext
287 lines
10 KiB
Plaintext
== 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: the constructor arguments are forwarded to build the underlying type:
|
|
[source,cpp]
|
|
----
|
|
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.
|
|
|
|
[source,cpp]
|
|
----
|
|
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.
|
|
|
|
== How to fix it
|
|
|
|
The rule reports forwarding constructors without proper constraints if they can be called with a single argument.
|
|
To eliminate this pitfall, add constraints to such constructors so that they are not considered an overload candidate when the argument is
|
|
a reference to the class itself. This can be achieved by adding any of the following checks to the forwarding reference constructor:
|
|
|
|
* a check of the concept `!std::same_as<std::remove_cvref_t<U>, Wrapper>`, or
|
|
* a check of type predicate `!std::is_same_v<std::remove_cvref_t<U>, Wrapper>`, or
|
|
* an `std::enable_if` with the equivalent condition.
|
|
|
|
Note that special care has to be taken when `Wrapper` is a base class. This is explained in more detail in "Going the extra mile"
|
|
below. In this case, those checks would become:
|
|
|
|
* the concept `!std::derived_from<std::remove_cvref_t<U>, Wrapper>`, or
|
|
* a type-predicate `!std::is_base_of_v<Wrapper, std::remove_cvref_t<U>>`, or
|
|
* an `std::enable_if` with the equivalent condition.
|
|
|
|
The concept-based solutions require {cpp}20. The `std::enable_if` solution is more cumbersome to write but can always be used.
|
|
|
|
Note that there are other ways to constrain such a constructor, but this rule only recognizes the explicit checks described above as compliant.
|
|
|
|
=== Code examples
|
|
|
|
==== Noncompliant code example
|
|
|
|
In this noncompliant example, the implicitly compiler-generated copy constructor can receive calls only when copying non-const lvalues (i.e., exact
|
|
matches). Otherwise, the forwarding constructor is used, even when the given type can not be used to initialize the wrapped
|
|
`std::string` object.
|
|
|
|
// No diff-ids because the first example has two compliant solutions.
|
|
[source,cpp]
|
|
----
|
|
class Wrapper {
|
|
public:
|
|
template <typename T>
|
|
Wrapper(T&& str) // Noncompliant: competes with compiler-generated copy constructor
|
|
: str(std::forward<T>(str)) {}
|
|
|
|
private:
|
|
std::string str;
|
|
};
|
|
----
|
|
|
|
==== Compliant solution
|
|
|
|
We fix the problem by adding a constraint to our forwarding constructor. This enables the copy constructor to receive calls again by
|
|
excluding the forwarding constructor when the deduced `T` is `Wrapper` (after discarding references and const-volatile qualifiers).
|
|
|
|
[source,cpp]
|
|
----
|
|
class Wrapper {
|
|
public:
|
|
template <typename T>
|
|
requires (!std::same_as<Wrapper, std::remove_cvref_t<T>>)
|
|
Wrapper(T&& str) // Compliant: no longer competes with the copy constructor
|
|
: str(std::forward<T>(str)) {}
|
|
|
|
private:
|
|
std::string str;
|
|
};
|
|
----
|
|
|
|
If {cpp}20 is not available, we can use `std::enable_if` instead of concepts. We also can not use `std::remove_cvref_t`, and we have to
|
|
be more verbose:
|
|
|
|
[source,cpp]
|
|
----
|
|
// Define our own remove_cvref_t for use in C++11
|
|
template <typename T>
|
|
using remove_cvref_t = typename std::remove_cv<typename std::remove_reference<T>::type>::type;
|
|
|
|
class Wrapper {
|
|
public:
|
|
template <
|
|
typename T,
|
|
typename std::enable_if<
|
|
!std::is_same<Wrapper, remove_cvref_t<T>>::value, int>::type /* Unnamed */ = 0>
|
|
Wrapper(T&& str) // Compliant: no longer competes with the copy constructor
|
|
: str(std::forward<T>(str)) {}
|
|
|
|
private:
|
|
std::string str;
|
|
};
|
|
----
|
|
|
|
==== Noncompliant code example
|
|
|
|
This noncompliant example demonstrates a bad attempt at constraining a forwarding constructor in a template wrapper:
|
|
|
|
[source,cpp,diff-id=1,diff-type=noncompliant]
|
|
----
|
|
template<typename T>
|
|
class TemplateWrapper {
|
|
public:
|
|
TemplateWrapper(TemplateWrapper const& other) = default;
|
|
|
|
template<typename U>
|
|
requires std::constructible_from<T, U>
|
|
TemplateWrapper(U&& u) // Noncompliant: constructible_from check is not sufficient in general
|
|
: value(std::forward<U>(u))
|
|
{}
|
|
|
|
private:
|
|
T value;
|
|
};
|
|
----
|
|
|
|
The problem with this constraint is that it depends on how the type `T` can be constructed; For example, it can yield unexpected results if
|
|
`T` itself has a forwarding constructor.
|
|
|
|
==== Compliant solution
|
|
|
|
In order to properly make our `TemplateWrapper` generic, we need to add the necessary constraint alongside `std::constructible_from`:
|
|
|
|
[source,cpp,diff-id=1,diff-type=compliant]
|
|
----
|
|
template<typename T>
|
|
class TemplateWrapper {
|
|
public:
|
|
TemplateWrapper(TemplateWrapper const& other) = default;
|
|
|
|
template<typename U>
|
|
requires (!std::derived_from<std::remove_cvref_t<U>, TemplateWrapper> && std::constructible_from<T, U>)
|
|
TemplateWrapper(U&& u) // Compliant: properly constrained regardless of how T can be constructed
|
|
: value(std::forward<U>(u))
|
|
{}
|
|
|
|
private:
|
|
T value;
|
|
};
|
|
----
|
|
|
|
Using `std::derived_from` instead of `std::same_as` is only meant for demonstration purposes here. `std::derived_from` is necessary only if
|
|
`TemplateWrapper` has derived classes, to ensure that the copy constructors of these derived classes don't end up calling the forwarding
|
|
constructor. This is explained in more detail in the "Going the extra mile" section below.
|
|
|
|
==== Noncompliant code example
|
|
|
|
In this noncompliant example, the forwarding constructor accepts a parameter pack and uses it to initialize the wrapped type. This can
|
|
still compete with the copy constructor when called with a single argument. Using `std::constructible_from` is not sufficient for the same
|
|
reasons as the previous example.
|
|
|
|
[source,cpp,diff-id=2,diff-type=noncompliant]
|
|
----
|
|
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
|
|
|
|
In this case, we can use a type tag to allow the user to explicitly choose the emplace constructor.
|
|
This approach is simpler to implement and offers greater flexibility.
|
|
It is the same approach used by many wrapper types in the standard library,
|
|
such as https://en.cppreference.com/w/cpp/utility/optional/optional[`std::optional`]
|
|
and https://en.cppreference.com/w/cpp/utility/expected/expected[`std::expected`].
|
|
|
|
[source,cpp,diff-id=2,diff-type=compliant]
|
|
----
|
|
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: use type tag to explicitly choose emplace constructor
|
|
: value(std::forward<Args>(args)...)
|
|
{}
|
|
|
|
private:
|
|
T value;
|
|
};
|
|
----
|
|
|
|
=== Going the extra mile
|
|
|
|
When the forwarding constructor belongs to a base class, using the `same_as` constraint check is not sufficient:
|
|
The forwarding constructor can still get selected when we are copying from a derived object.
|
|
|
|
[source,cpp]
|
|
----
|
|
class Base {
|
|
public:
|
|
template <typename T>
|
|
requires (!std::same_as<std::remove_cvref_t<T>, Base>) // Incorrect: same_as is not sufficient for base classes.
|
|
Base(T&& str) : str(std::forward<T>(str)) {}
|
|
private:
|
|
std::string str;
|
|
};
|
|
|
|
class Derived : public Base {};
|
|
----
|
|
|
|
Then the following results in a compilation error:
|
|
|
|
[source,cpp]
|
|
----
|
|
Derived d("str");
|
|
// Note that the constraint is satisfied when T is Derived&
|
|
Base b(d); // Calls the forwarding constructor instead of the usual "slicing" behavior
|
|
----
|
|
|
|
Additionally, subclasses can run into trouble when they try to define their copy constructors:
|
|
|
|
[source,cpp]
|
|
----
|
|
class Derived2 : public Base {
|
|
// ...
|
|
public:
|
|
Derived2(Derived2 const& d)
|
|
// d is of Derived2 type and it therefore satisfies the same_as constraint for the forwarding constructor
|
|
: Base(d) { // Error: Calls the forwarding constructor instead of the base copy constructor
|
|
// ...
|
|
}
|
|
};
|
|
----
|
|
|
|
To avoid these problems, use `std::derived_from` or `std::base_of` checks instead of `std::same_as` or `std::is_same` when the forwarding
|
|
constructor belongs to a class that has derived classes.
|
|
|
|
|
|
== Resources
|
|
|
|
=== Documentation
|
|
|
|
* {cpp} reference - https://en.cppreference.com/w/cpp/utility/forward[`std::forward`]
|
|
* {cpp} reference - https://en.cppreference.com/w/cpp/language/overload_resolution#Ranking_of_implicit_conversion_sequences[Ranking of implicit conversion sequences during overload resolution]
|
|
|
|
=== Articles & blog posts
|
|
|
|
* Effective Modern {cpp} item 26: Avoid overloading on universal references
|
|
* Eric Niebler - https://ericniebler.com/2013/08/07/universal-references-and-the-copy-constructo/[Universal References and the Copy Constructor]
|