Question:
When someone starts to study the scope of the C++11 standard, they usually come across the term " perfect forwarding " (sorry for not putting the translation but it is almost obvious that the documentation in Spanish is not abundant and the literal translation does not convince me).
The fact is that this terminology is used a lot when talking about templates.
What are the advantages of std::forward
?
In the example below, what benefits (assuming there are any) does using std::forward
give us?
template<class T>
void funcion1(T&& parametro)
{
otraFuncion(std::forward<T>(parametro));
}
template<class T>
void funcion2(T&& parametro)
{
otraFuncion(parametro);
}
Answer:
If given a function template func1(param1, param2)
we want to call a function func2(param1,param2)
using the c++03 standard, we could opt for a first version such that:
template<class Param1, class Param2>
void func1(Param1 param1, Param2 param2)
{
func2(param1,param2);
}
The problem with this version is that if instead of native types ( int
, float
, etc) we pass an object, for example a string, the code will make an intermediate copy of the object. We then proceed to improve the template to avoid the problem:
template<class Param1, class Param2>
void func1(Param1& param1, Param2& param2)
{
func2(param1,param2);
}
It works quite well with objects… but now we are no longer able to do something like:
func1(10,5.5);
Since we can't get a reference from an rvalue
. What if we force references to be constant?
template<class Param1, class Param2>
void func1(Param1 const& param1, Param2 const& param2)
{
func2(param1,param2);
}
Ok, the previous problem has been fixed but we have a new problem… the code will fail if func2
supports non-constant references:
void func2(int& a, int& b);
int a = 2, b = 0;
func1(a,b); // ERROR: func2 requiere referencias no constantes
The problem begins to grow, but it seems that there is still a solution. How about we make use of const_cast
? This way we remove the const
modifier:
template<class Param1, class Param2>
void func1(Param1 const& param1, Param2 const& param2)
{
func2(const_cast<Param1&>(param1),const_cast<Param2&>(param2));
}
It seems that now everything works… wait… no, there is something wrong. What happens if we feed the template with constant elements?
const int a = 2, b = 0;
func1(a,b);
In this case the result is indeterminate. In some cases it will appear to work fine and in others the program will fail miserably. The reason? That constant objects can end up stored in read-only memory locations and it is the operating system itself that is responsible for checking that said restriction is met.
As a last solution we can choose to create a complete set of specializations to contemplate all the previous possibilities. In this way the compiler will choose the most appropriate for each situation and the program will work without problems:
template<class Param1, class Param2>
void func1(Param1 & param1, Param2 & param2)
{
func2(param1,param2);
}
template<class Param1, class Param2>
void func1(Param1 const& param1, Param2 & param2)
{
func2(param1,param2);
}
template<class Param1, class Param2>
void func1(Param1 & param1, Param2 const& param2)
{
func2(param1,param2);
}
template<class Param1, class Param2>
void func1(Param1 const& param1, Param2 const& param2)
{
func2(param1,param2);
}
The obvious problem with this last solution is that the number of implementations required grows exponentially according to the number of arguments to deal with… and all despite the fact that the implementation is exactly the same.
std::forward
appeared in the C++11 standard precisely to cover this niche. The implementation of std::forward
according to the standard (see §20.2.3 ) is as follows:
Returns: static_cast(t).
And with this fragment all the errors are solved at once and as a demonstration the following code:
template<class T>
void func2(T &)
{ std::cout << "func2(T &)\n"; }
template<class T>
void func2(T const&)
{ std::cout << "func2(T const&)\n"; }
template<class T>
void func2(T &&)
{ std::cout << "func2(T &&)\n"; }
template<class T>
void func(T&& a)
{
func2(std::forward<T>(a));
}
int f()
{ return 7; }
template<class Func>
void Test(
const std::string& label,
Func f)
{
std::cout << std::setfill(' ') << std::setw(14) << std::left << label << " --> ";
f();
}
int main()
{
Test("func(T)", [](){int a; func(a); });
Test("func(T &)", [](){int a; int& b = a; func(b); });
Test("func(T const&)", [](){int a; const int& b = a; func(b); });
Test("func(T &&)", [](){ func(1); });
Test("func(T &&)", [](){ func(f()); });
return 0;
}
And its corresponding result:
func(T) --> func2(T &)
func(T &) --> func2(T &)
func(T const&) --> func2(T const&)
func(T &&) --> func2(T &&)
func(T &&) --> func2(T &&)
Additional tests can be done on the previous example, commenting out one of the three implementations of func2
and we will see how the program is able to readjust its behavior by providing a solution based on the existing options.
Thus, if we comment func2(T&)
the output will be:
func(T) --> func2(T &&)
func(T &) --> func2(T &&)
func(T const&) --> func2(T const&)
func(T &&) --> func2(T &&)
func(T &&) --> func2(T &&)
And if we comment func2(T const&)
instead, the output will be:
func(T) --> func2(T &)
func(T &) --> func2(T &)
func(T const&) --> func2(T &)
func(T &&) --> func2(T &&)
func(T &&) --> func2(T &&)
In the latter case, the conversion of T const&
to T &
may attract attention. What happens in this case is that T
is going to be a constant, so if func2(T&)
tries to make changes to the attribute, a compile-time error will occur.
The result, as you can see, is that std::forward
provides a fairly simple and elegant mechanism for resolving the dilemma of deciding between rvalue
and lvalue
.
And what happens if we remove std::forward
in the example?
template<class T>
void func(T&& a)
{
func2(a);
}
Well, the output will now vary significantly:
func(T) --> func2(T &)
func(T &) --> func2(T &)
func(T const&) --> func2(T const&)
func(T &&) --> func2(T &)
func(T &&) --> func2(T &)
The main effects are that the overload that makes use of the move syntax will no longer be accessible, so a decrease in the performance of the code is expected, especially if the function receives objects whose copy is heavy. This version of the function will only be available if one of the two previous versions is removed ( func2(T&)
or func2(T const&)
) and we will not be able to renounce these overloads more than once, so we will have a problem.
Could a similar solution be applied in versions prior to C++11?
Unfortunately not. This entire solution revolves around the move syntax and the differentiation between rvalue
and lvalue
. The is not available in earlier standards and the latter has undergone serious revisions in the C++11 standard. In C++03 you could tell an rvalue
from an lvalue
but the process is neither clean nor pretty (see Conditional Love: FOREACH Redux ).
So, recap:
What are the advantages of std::forward?
The reason for resorting to std::forward
is to have a tool that allows us, within a template, to distinguish an rvalue
from an lvalue
since this avoids us having to resort to the tedious task of repeating code in different implementations of the template .
What advantages (assuming there are any) does using std::forward give us?
By being able to distinguish between lvalue
and rvalue
, the compiler will be able to call the most appropriate function depending on the occasion.