An overview of C++ perfect forwarding

Intro

Perfect forwarding is when a wrapper function template passes lvalueness/ravlueness of its arguments to a wrapped function. To do so std::forward is used:

#include <iostream> using namespace std; void Display(int& i){ cout<< i<<" int&"<<endl; } void Display(int&& i){ cout<< i<<" int&& called"<<endl; } template<class T> void DisplayWrapper(T&& t){ Display(forward<T>(t)); } int main(){ int x=5; DisplayWrapper(5); // int&& called  DisplayWrapper(x); // int&  return 0;} 

In the above example, DisplayWrapper passes prvalue, 5, and lvalue x to the correct overload of Display. This desired behaviour is conducted with std::forward. For more details read the following sections.

Prerequisite

Here I assume, you are familiar with lvalue, rvalue, rvalue reference and std::move. If not, I recommend google them or have a quick look at my post here where I briefly explained them all together.

Problem

If we run the above example without std::forward, we have:

template<class T> void DisplayWrapper(T&& t){ Display(t); } 

The outcome will be:

DisplayWrapper(5); // 5 int& DisplayWrapper(x); // 5 int& 

So DisplayWrapper doesn’t pass 5 as a prvalue to Display function but as an lvalue.

Overloading

Let’s only focus on Display functions, and see which overload is selected with different parameters:

#include <iostream> using namespace std; void Display(int& i){ cout<< i<<" int& called"<<endl; } void Display(int&& i){ cout<< i<<" int&& called"<<endl; } int main(){ int x=5; int&& y=10; Display(5); // int&& called  Display(move(y)); // int&& called  Display(move(x)); // int&& called  Display(x); // int& called  Display(y); // int& called  return 0; } 

To sum it up:

  • if we pass an lvalue: x and y, the first overload is called,
  • if we pass an rvalue: prvalue (5) and xvalue (move(x) and move(y)), the second overload is called.

Note that y is a named rvalue reference, so it is an lvalue. On the other hand, the outcome of std::move is an unnamed rvalue reference, therefore, it’s an rvalue (or technically xvalue).

Universal reference

In a function with the template parameter T, the argument T&& is called universal reference as it can be lvalue reference or rvalue reference.

void Display(int& i){ cout<< i<<" int& called"<<endl; } void Display(int&& i){ cout<< i<<" int&& called"<<endl; } template<class T> void DisplayWrapper(T&& t){ Display(t); } int main(){ int x=5; DisplayWrapper(x); // line 1: Display(int&) called  DisplayWrapper(5); // line 2: Display(int&) called!!!  return 0; } 

So at line 1, t can bind to x, then it has the type of int&, lvalue reference. At line 2, t bind to 5 and its type is int&&, rvalue reference.

When passing t to Display, in the first case the correct overload Display(int& ) is called. In the second case, still, the same overload is called because t in any case is an lvalue (it has a name and address). So somehow, we lost the rvalueness of 5 within DisplayWrapper.

Solution

To ensure that an rvalue in outer scope is still an rvalue inside the wrapper function, we apply std::forward on the universal reference (see the first example in the Intro section). All in all, std::forward forwards an lvalue as lvalue and an rvalue as rvalue.

Details

The universal reference has only the form of auto&& and T&& where T is a template type. Therefore, vector<T>&&, const T&&, MyClass<T>&& are not universal references, they are rvalue references.

Moreover, for T&& to be a universal reference, T must be deduced by the function call, not class instansiation:

template<class T> struct A { void DoIt(T&& t){}; } A<int> a; // T is decided. 

Now a call to a.DoIt is like

void DoIt(int&& t){} 

which is not a universal reference.

I feel for everyday coding the info here is enough on the subject, but there are a lot of details. If you are interested, start from references down here.

References

Scott Meyers on Isocpp Modernes C++ cppreference

Tags ➡ C++

Latest Posts

Comments

0 comment