Forwarding References and Perfect Forwarding¶
Important Article that Explains Forwarding References and Perfect Forwarding¶
Forwarding References¶
The same notation used for rvalue references, the double ampersand &&, is also used to denote a forwarding reference. However, a forwarding reference is always a template function parameter of a generic type T&&
such as that below
template<typename T> void f(T&& t);
While T&&
looks just like an rvalue reference (Rvalue References and Lvalue References in C++), when &&
is used for a function template parameter, it is called a forwarding refernence, and unlike an rvalue reference, a forwarding reference can bind to
both rvalues and lvalues, to const and non-const, to volatile, to everything. Its purpose is to support argument forwarding: to allow you to pass the argument on unchanged to other function(s). An example of argument forwarding is the emplace
method of many STL containers.
template<typename... ARGS>emplace (ARGS&&... args)
allows a new container element to be constructed by forwarding its constructor parameters directly to placement new avoiding unnecessary copy or move operations.
Forwarding reference take advantage of the new C++11 reference collapsing rules. In C++11, unlike previous versions, you can syntactically have a reference to a reference, and the following reference collapsing rules apply:
T& & becomes T&
T& && becomes T&
T&& & becomes T&
T&& && becomes T&&
Except in one case, of T&& &&
, the final result of reference collapsing is always T&
.
The Purpose of Forwarding References¶
Unlike an rvalue reference, a forwarding reference T&&
can bind to both rvalues and lvalues. It can bind to both const and non-const objects. It can bind to mutable and volitale. It can bind to everything. When a lvalue, say, of
type X is passed to a template function with a forwarding reference T&&
, then T
becomes X&
, and therefore T&&
becomes X& &&
, which after applying the reference collapsing rules becomes simply X&
. On the other hand, when an rvalue
of type X is passed, T
becomes X
, and T&&
is simply X&&
.
Thus an lvalue of type X binds as X&
and an rvalue of type X binds as X&&
. We can see this in the code below:
#include <vector>
template<class T> struct state_type {
using type = T;
static void describe()
{
cout << "In non-specialization of struct state_type<T>" << endl;
}
};
template<class T> struct state_type<T&> {
using type = T;
static void describe()
{
cout << "In partial template specialization of struct state_type<T&>" << endl;
}
};
template<class T> struct state_type<T&&> {
using type = T;
static void describe()
{
cout << "In partial template specialization of struct state_type<T&&>" << endl;
}
};
template<class ARG> void f(ARG&& arg)
{
state_type<ARG>::describe();
}
using namespace std;
vector<int> v{1, 2, 3, 4};
f(v);
f(vector<int>{5, 6, 7, 8});
f(move(v));
This will result in output of:
In partial template specialization of struct state_type<T&>
In non-specialization of struct state_type<T>
In non-specialization of struct state_type<T>
For the lvalue v in f(v);
, ARG
resolves to vector<int>&
, and the instantiation of f()
is
void f(vector<int>& && arg)
{
state_type<vector<int&>::describe();
}
which, after applying reference collapsing rules for references, becomes
void f(vector<int>& arg)
{
state_type<vector<int&>::describe();
}
So we see arg binds as an lvalue reference. In the case of f(vector<int>{5, 6, 7, 8});
, ARG
resolves to vector<int>
, and the instantiation of f
looks like this:
void f(vector<int>&& arg)
{
state_type<vector<int>>::describe();
}
In this case arg
binds as a rvalue reference. We can use these binding rules for function templates as the first step in writing a template function, like a factory method, that perfectly forwards its parameters leaving the paramters type intact.
Now take this factory method:
class A { // trivial example
std::string str;
public:
A(const std::string& lhs) : str(lhs) // copy constructor
{
cout << " A::A(std::string& lhs) invoked." << endl;
}
A(std::string&& lhs) // move constructor
{
cout << " A::A(std::string&& lhs) invoked." << endl;
}
};
template<class T, class ARG> std::shared_ptr<T> factory(ARG&& arg)
{
state_type<ARG>::describe();
return std::shared_ptr<T>{std::make_new<T>(arg)}; // fails to invoke A(string&&) when arg is an rvalue.
}
Note the output of the code below, where first an lvalue is passed to factory<T>(ARG&& arg)
and then an rvalue:
string lvaluestr{"lvaluestr"};
shared_ptr<A> ptr1 { factory<A>(lvaluestr) };
shared_ptr<A> ptr2 { factory<A>(string{"rvaluestr"}) };
The output is:
In partial template specialization of struct state_type<T&>
A::A(std::string& lhs) invoked.
In non-specialization of struct state_type<T>
A::A(std::string& lhs) invoked.
factory<T>(ARG&& arg)
correctly forwarded the lvalue reference, but not the rvalue reference. Instead the rvalue reference got passed as lvalue references. Why did shared_ptr<A> ptr2 { factory<A>(string{"rvaluestr"}) };
fail in invoking A::A(A&&)
?
The reason is, arg
is not an rvalue within the body of factory–even though the type of arg
is rvalue reference to std::string! Remember an rvalue reference parameter, since it has a name, is an lvalue. We need to remove the
name with a cast:
template<class T, class ARG> std::shared_ptr<T> factory(ARG&& arg)
{
state_type<ARG>::describe();
return std::shared_ptr<T>{ new T( static_cast<ARG&&>(arg) ) }; // static_cast<ARG&&>(arg) returns a nameless parameter.
}
Now when lvaluestr
is passed, ARG
becomes string&
and so ARG&&
becomes string& &&
, which, after applying the reference collapsing rules, becomes simply string&
, and static_cast<string&>(arg)
is still an lvalue. When an
rvalue is passed, however, the lvalue arg
parameter is cast to a nameless rvalue.
The standard library provides forward<T>(std::remove_reference<T>::type&)
to do this static_cast, and it looks like this:
template<class T>
T&& forward(typename remove_reference<T>::type& a) noexcept
{
return static_cast<T&&>(a);
}
If you use just T&
instead of remove_reference<T>::type&
in the defintion of std::forward
, perfect forwarding still works just fine. However, as Thomas Becker explains:
“it works fine only as long as we explicitly specify ARG as the template argument of std::forward
, std::forward<ARG>
. The purpose of the remove_reference
in the definition of std::forward is to force us to do so.” If we don’t explicitly
supply the template argument when invoking forward()
, this gcc compile error results:
template<class _Tp> void f(_Tp&& t)
{
cout << "t = " << forward(t);
}
f(10);
results in:
/usr/include/c++/7/bits/move.h:73:5: note: candidate: template<class _Tp> constexpr _Tp&& std::forward(typename std::remove_reference<_From>::type&)
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
^~~~~~~
/usr/include/c++/7/bits/move.h:73:5: note: template argument deduction/substitution failed:
main.cpp:74:30: note: couldn't deduce template parameter ‘_Tp’
cout << "t = " << forward(t);
Returning to our original example
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{
return static_cast<_Tp&&>(__t);
}
we now use std::forward
in our factory() function:
string lvaluestr{"lvaluestr"};
shared_ptr<A> ptr1 { factory<A>(lvaluestr) };
shared_ptr<A> ptr2 { factory<A>(string{"rvaluestr"}) };
template<class T, class ARG> std::shared_ptr<T> factory(ARG&& arg)
{
state_type<ARG>::describe();
return std::shared_ptr<T>{ new T( std::forward<T>(arg) ) }; // forward returns a nameless parameter.
}
The output now is:
In partial template specialization of struct state_type<T&>
A::A(std::string& lhs) invoked.
In non-specialization of struct state_type<T>
A::A(std::string&& lhs) invoked.
When factory<A>(lvaluestr)
is called, again, ARG
resolves to string&
and applying reference collapsing, we have this instantiation of factory:
std::shared_ptr<A> factory(string& arg)
{
return std::shared_ptr<A>{ new A( std::forward<T>(arg) ) };
}
For the accompanying forward instantiation, the partial template specialization for lvalue references is applied and std::remove_reference<string&>::type&
resolves to string&
and so forward()
gets instantiated as:
constexpr string& forward(string& __t) noexcept
{
// static_cast<string& &&> collpases to static_cast<string&> below:
return static_cast<string&>(__t);
}
So the complete instantiation of factory<A>(lvaluestr)
is
std::shared_ptr<A> factory(string& arg)
{
return std::shared_ptr<A>{ new A( static_cast<string&>(arg) ) };
}
which results in the A::A(std::string&)
being invoked!
When factory<A>(string{"rvaluestr})
is called, again, ARG
resolves to string
, and we have this instantiations of factory:
std::shared_ptr<A> factory(string&& arg)
{
return std::shared_ptr<A>{ new A( std::forward(arg) ) };
}
and the accompanying instantiation of forward:
// remove_reference<string>::type& resolved to string&
constexpr string&& forward(string& __t) noexcept
{
return static_cast<string&&>(__t);
}
So finally factory<A>(string{"rvaluestr})
resolves to:
std::shared_ptr<A> factory(string&& arg)
{
return std::shared_ptr<A>{ new A( static_cast<string&&>(arg) ) };
}
which will cause the A::A(string&&)
constructor will be invoked!
Application of Perfect Forwarding¶
Below temple<calss T> class Vector
has a new template member function emplace_back
that takes variadic forwarding references.
template<class T> Vector {
// snip...(as above)
public:
// snip...(as above)
template<class... ARGS> void emplace_back(ARGS&& ... args);
};
template<class T> template<class... ARGS> void Vector<T>::emplace_back(ARGS&& ... args)
{
if (current == size) { // If new value won't fit...
grow(); // ...grow the vector
}
new(p + current) T{std::forward<ARGS>(args)...}; // Use placement new to construct the object in existing memory.
current++;
}
class Employee {
std::string name
int age;
int salary;
public:
Employee() {}
Employee(const std::string& _name, int _age, int _salary) :\
name{_name}, age{_age}, salary{_salary} {}
Employee(Employee&& e) : name{std::move(e.name)}, age{e.age}, salary{e.salary} {}
// snip...
};
Vector<Employee> v;
v.push_back(Employee{"John Doe", 15, 0});
v.emplace_back("Bob Smith", 45, 80000);
emplace_back()
creates the new vector element in-place, in the vector’s already-allocated memory, using the forwarded parameters. This eliminates the creation and moving of a temporary object into the vector.
Overloading involving both rvalues and forwarding references¶
What happens when a function template is overloaded with two versions: one taking an lvalue reference and the other a forwarding reference? For example:
template<typename T> void g(T& param) noexcept
{
cout << "In g(T& param)" << endl;
}
template<typename T> void g(T&& param) noexcept
{
cout << "In g(T&& param)" << endl;
}
int x = 20;
g(11);
g(x);
In this case, the lvalue reference version always wins, so the output would be:
In g(T&& param)
In g(T& param)
However, if we remove the overload on the lvalue reference See these links for move above forwarding references:
template<typename T> void g(T&& param) noexcept
{
cout << "In g(T&& param)" << endl;
}
int x = 20;
g(11);
g(x);
Then the output is:
In g(T&& param)
In g(T&& param)
as one would expect.
auto&&¶
auto&&
used in range-base for loops
for(auto&& x : c) { // x is a forwarding reference
//...
}
is also a forwarding reference. It’s purpose is support the abstraction of perfect forwarding of x to other functions within the for loop.
Further articles on forwarding references:¶
Conclusion¶
When an lvalue is passed to std::forward<T>(x)
, it returns a nameless lvalue; however, when an rvalue is passed, a nameless rvalue is returned. std::forward<T>()
thus perfectly fowards template function parameters that are specified as
forwarding parameters, resulting in the correct method always being invoked.