Why Do We Need Perfect Forwarding#
Here is an example of a class factory function:
template <typename T, typename Arg>
std::shared_ptr<T> factory(Arg arg) {
return std::shared_ptr<T>( new T(arg));
}
In the above example, the argument object arg
is passed by value, which incurs the cost of creating additional temporary objects1. To avoid this, we can change it to pass by reference:
template <typename T, typename Arg>
std::shared_ptr<T> factory(Arg &arg) {
return std::shared_ptr<T>( new T(arg));
}
However, this implementation has a problem of not being able to bind to rvalue arguments. For example, factory<X>(42)
will result in a compilation error. To further improve it, we can use const reference:
template <typename T, typename Arg>
std::shared_ptr<T> factory(const Arg &arg) {
return std::shared_ptr<T>( new T(arg));
}
The problem with this implementation is that it does not support move semantics. By using rvalue reference for the function parameter, we can solve the problem of perfect forwarding.
Reference Collapsing#
Before C++11
, we couldn't have a reference type refer to another reference type. However, with the introduction of rvalue references in C++
, this practice has been relaxed2, resulting in the reference collapsing rule, which allows us to have references that can be both lvalue references and rvalue references. However, it follows the following rules:
Function Parameter Type | Argument Type | Deduced Function Parameter Type |
---|---|---|
T& | lvalue reference | T& |
T& | rvalue reference | T& |
T&& | lvalue reference | T& |
T&& | rvalue reference | T&& |
Template Argument Type Deduction#
For a function template template<typename T>void foo(T&&);
, applying the reference collapsing rule mentioned above, we can conclude the following:
- If the argument is an lvalue of type A, then the type of template parameter T is
A&
, and the type of the function parameter isA&
. - If the argument is an rvalue of type A, then the type of template parameter T is
A&&
, and the type of the function parameter isA&&
.
This also applies to type deduction in member function templates of class templates:
template <class T> class vector {
public:
// T is a class template parameter ⇒ this member function does not require type deduction; the function parameter type here is an rvalue reference to T
void push_back(T &&x);
// This member function is a function template with its own template parameter, requiring type deduction
template <typename Args> void emplace_back(Args &&args); }
The function parameter of a function template must be in the form of T&& to require template argument type deduction. Even if the function parameter is declared as const T&&, it can only be used literally and does not require template argument type deduction.
Perfect Forwarding#
Here is the implementation of the perfect forwarding version of the above code:
template <typename T, typename Arg>
shared_ptr<T> factory(Arg&& arg) {
return std::shared_ptr<T>( new T(std::forward<Arg>(arg)) );
}
Where std::forward
is a template function defined in the standard library <utility>
:
template< class T > T&& forward( typename std::remove_reference<T>::type& t ) {
return static_cast<T&&>(t);
}
template< class T > T&& forward( typename std::remove_reference<T>::type&& t ) {
return static_cast<T&&>(t);
}
std::remove_reference
is a class template defined in the standard library <type_traits>
, used to remove the reference from a type. The type type
defined in it is the underlying type of the reference.
template< class T > struct remove_reference {typedef T type;};
template< class T > struct remove_reference<T&> {typedef T type;};
template< class T > struct remove_reference<T&&> {typedef T type;};
- When the data type of the argument is an lvalue reference type
S&
(T = S&
), the type oft
isS&
, andstatic_cast<S& &&>(t)
collapses tostatic_cast<S&>(t)
. - When the data type of the argument is an rvalue reference type
S&&
(T = S&&
), the type oft
isS&&
, andstatic_cast<S&& &&>(t)
collapses tostatic_cast<S&&>(t)
.
References#
https://en.wikipedia.org/wiki/Reference_collapsing