std::forward, Perfect Forwarding of Parameters

C++11 introduced the std::forward to preserve the value category (lvalue or rvalue) of a cv-unqualified function template parameter, that is declared as an rvalue reference, in time of passing that to another function. It forwards an lvalue either as an lvalue or as an rvalue depending on the deduced type of the template parameter.

Apparently complicated stuff. But we can understand it easily in steps.

Forwarding Problem

To understand perfect forwarding, let’s start the forwarding problem.

#include <iostream>
using namespace std;

void equivalent(int& a) {
    cout << "int&: " << a << endl;
}

void equivalent(const int& a) {
    cout << "const int&: " << a << endl;
}

int main() {
    int x = 10;
    equivalent(x);
    
    equivalent(30);
    return 0;
}
$ g++ -o test test.cpp 
$ ./test 
int&: 10
const int&: 30

We have an overloaded function, equivalent(). One version take an int reference and a const int reference as input.

In main(), we called this function twice – first with an lvaue (variable x) and then with an rvalue (integer literal 30).

Compiler could figure out the right version of the overloaded function to call in both cases without any ambiguity.

So far, so good.

Now we want to have a wrapper template function that will call this overloaded equivalent() function.

#include <iostream>
using namespace std;

void equivalent(int& a) {
    cout << "int&: " << a << endl;
}

void equivalent(const int& a) {
    cout << "const int&: " << a << endl;
}

template <typename T>
void wrapper(T& a) {
    equivalent(a);
}

int main() {
    int x = 10;
    wrapper(x);
    
    wrapper(30); /* Will produce compilation error*/
    return 0;
}

We might assume that compiler will deduce appropriate type for both wrapper(x) and wrapper(30) calls. But wrapper(30) will produce a compilation error.

Why? ‘T&‘ is considered as a non-const lvalue reference. Before C++11, there was only type of reference but now this type of reference is called lvalue reference. A non-const lvalue reference can be bound with an lavaue but not with an rvalue. Integer literals, like 30, are considered as rvalue(s).

So, we can make the input of wrapper() function parameter as cont lvalue referencevoid wrapper(T& a) to void wrapper(const T& a). A const lvalue reference can be bound to both an lvalue and an rvalue. Now, we’ll not have any compilation error.

But this would add another problem.

#include <iostream>
using namespace std;

void equivalent(int& a) {
    cout << "int&: " << a << endl;
}

void equivalent(const int& a) {
    cout << "const int&: " << a << endl;
}

template <typename T>
void wrapper(const T& a) {
    equivalent(a);
}

int main() {
    int x = 10;
    wrapper(x);
    
    wrapper(30);
    return 0;
}
$ ./test 
const int&: 10
const int&: 30

From the output, we can see that in both the calls, the second overloaded function, equivalent(const int& a), was called. We expected equivalent(int& a) to be called for wrapper(x). But it did not happen because ‘a‘ becomes of type const int reference inside wrapper(). So, the compiler always resolve the equivalent(const int& a) overloaded function.

When we directly called equivalent(), two different functions had been called for ‘x‘ and ’30’. So, the equivalence is broken when the calls are made via wrapper().

One Solution – but so good one

One way to solve this problem is to have have two wrapper template functions – one with ‘T&‘ and another with ‘const T&‘.

#include <iostream>
using namespace std;

void equivalent(int& a) {
    cout << "int&: " << a << endl;
}

void equivalent(const int& a) {
    cout << "const int&: " << a << endl;
}

template <typename T>
void wrapper(T& a) {
    equivalent(a);
}

template <typename T>
void wrapper(const T& a) {
    equivalent(a);
}

int main() {
    int x = 10;
    wrapper(x);
    
    wrapper(30);
    return 0;
}
$ ./test 
int&: 10
const int&: 30

This apparently solved the problem. But there is a caveat.

So far we had only one input parameter. If we have 3, then we’ll need to have 9 combinations.

template <typename T1, typename T2, typename T3>
void wrapper(T1& a, T2& b, T3& c);

template <typename T1, typename T2, typename T3>
void wrapper(const T1& a, T2& b, T3& c);

template <typename T1, typename T2, typename T3>
void wrapper(T1& a, const T2& b, T3& c);

template <typename T1, typename T2, typename T3>
void wrapper(T1& a, T2& b, const T3& c);

template <typename T1, typename T2, typename T3>
void wrapper(const T1& a, const T2& b, T3& c);

template <typename T1, typename T2, typename T3>
void wrapper(const T1& a, T2& b, const T3& c);

template <typename T1, typename T2, typename T3>
void wrapper(T1& a, const T2& b, const T3& c);

template <typename T1, typename T2, typename T3>
void wrapper(const T1& a, const T2& b, const T3& c);

For, N parameters, there will be N2 combinations which is completely unmanageable.

So, we need a better mechanism where a template function parameter will accept both lvalue and rvalue for the same parameter and retain its value category in time of calling another function from inside the template function.

Rvalue Reference and std::forward

Combination of rvalue reference and std::forward solves all the problems we discussed so far.

The rvalue reference, denoted as ‘&&‘, can be bound to an rvalue. But if an rvalue reference (&&) is used in a type declaration, it can sometimes mean rvalue reference, and sometimes either rvalue reference or lvalue reference. We’ll see an example later.

If ‘&&‘ is used with a deduced type (like template type or auto) the variable type will become either lvalue reference or rvalue reference depending on whether it is initialized with an lvaue or rlvaue.

#include <iostream>
using namespace std;

void equivalent(int& a) {
    cout << "int&: " << a << endl;
}

void equivalent(const int& a) {
    cout << "const int&: " << a << endl;
}

template <typename T>
void wrapper(T&& a) {
    equivalent(a);
}

int main() {
    int x = 10;
    wrapper(x);
    
    wrapper(30);
    return 0;
}
$ ./test 
int&: 10
int&: 30

Here we changed void wrapper(const T& a) to void wrapper(T&& a). Now ‘a’ can be initialized with either an lvaue (x) or an rvalue (30). There will be no compilation error.

Still there is a problem.

You can notice from the output that the equivalent(int& a) version of the function was called in both cases.

Let’s understand what happened here.

As ‘x‘ is an lvalue, the wrapper(x) call was type deduced to wrapper<int&>(int&). So, ‘T’ becomes ‘int&‘ and type of ‘a‘ becomes an integer lvalue reference. The equivalent(a) call was resolved to equivalent(int& a).

And the wrapper(10) call was type deduced to wrapper<int>(int&&). So here T becomes ‘int‘ and type of ‘a‘ becomes integer rvalue reference.

Now here is a very important thing to note: Even if the type of ‘a’ is an rvalue reference, ‘a’ itself is an lvalue.

The guideline is that if a variable has a name or we can get the memory location of the variable, the variable is an lvalue. As, ‘a‘ is an lvalue here also, the equivalent(int& a) version was called.

The Final Solution

We can solve this problem by static cast-ing ‘a‘ to ‘T&&‘, (static_cast<T&&>(a)), in time of calling equivalent().

#include <iostream>
using namespace std;

void equivalent(int& a) {
    cout << "int&: " << a << endl;
}

void equivalent(const int& a) {
    cout << "const int&: " << a << endl;
}

template <typename T>
void wrapper(T&& a) {
    equivalent(static_cast<T&&>(a));
}

int main() {
    int x = 10;
    wrapper(x);
    
    wrapper(30);
    return 0;
}
$ ./test 
int&: 10
const int&: 30

We simply changed the line equivalent(a) to equivalent(static_cast<T&&>(a)) inside wrapper(). From the output we can see the intended version of equivalent() was called.

How it solved the problem?

Okay, we discussed that T becomes ‘int&‘ for the wrapper(x) call, ‘a‘ will be cast-ed to ‘int&‘, an lvalue reference. ‘int& &&‘ becomes ‘int&’ as per the reference collapsing rule.

Here is the reference collapsing rule in sort.

If TR that is a reference to a type T:

TR   R
==============
T&   &  -> T& 
T&   && -> T& 
T&&  &  -> T& 
T&&  && -> T&&

For the wrapper(30) call, T becomes ‘int‘. So, ‘a‘ will be type cast-ed to ‘int&&‘ – an rvalue reference. As ‘a‘ is type cast-ed to an rvalue reference, equivalent(const int& a) version will be called.

So the static_cast<T&&>, magically forwarded the original value category of the parameter in time of calling another function. This is called perfect forwarding.

For this purpose, C++11 introduced std::forward which is equivalent to static_cast<T&&>. So, the equivalent(static_cast<T&&>(a)) can be changed to equivalent(std::forward<T>(a)).

One Last Thing

As C++11 introduced the rvalue reference (&&), the equivalent(const int& a) function should be changed to equivalent(int&& a).

The equivalent(int&& a) function will be able to take rvalue as input argument like the equivalent(const int& a) . But it has another benefit. The rvalue reference enables move semantics that can be used to transfer resources efficiently for complex type objects.

So, the final code should look like:

#include <iostream>
using namespace std;

void equivalent(int& a) {
    cout << "int&: " << a << endl;
}

void equivalent(int&& a) {
    cout << "const int&: " << a << endl;
}

template <typename T>
void wrapper(T&& a) {
    equivalent(forward<T>(a));
}

int main() {
    int x = 10;
    wrapper(x);
    
    wrapper(30);
    return 0;
}

Author: Srikanta

I write here to help the readers learn and understand computer programing, algorithms, networking, OS concepts etc. in a simple way. I have 20 years of working experience in computer networking and industrial automation.


If you also want to contribute, click here.

Leave a Reply

Your email address will not be published. Required fields are marked *

0
0
0
0
0
0