rspec/rules/S6458/cfamily/rule.adoc
2023-10-17 14:17:15 +02:00

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]