The std::forward<>
function was introduced in C++ to enable perfect forwarding, allowing arguments to be passed to another function while preserving their original lvalue or rvalue status. Let’s first discuss the need for std::forward<>
. Then, we will see how std::forward<>
works.
Why do we need std::forward<>
in C++?
#
To understand std::forward<>
, let’s build some background. Suppose there are two overloaded implementations of the DisplayData()
function.
void DisplayData(int& x)
{
std::cout << "Lvalue reference: " << x << std::endl;
}
void DisplayData(int&& x)
{
std::cout << "Rvalue reference: " << x << std::endl;
}
The first implementation i.e. DisplayData(int& x)
accepts in a lvalue reference as argument and another overloaded implementation i.e. DisplayData(int&& x)
accepts a rvalue referece as argument.
Now we also have a wrapper template function like this,
template <typename T>
void PrintLog(T&& arg)
{
// Calls DisplayData(), but doesn't preserve rvalue status
DisplayData(arg);
}
Here, the wrapper template function PrintLog()
takes a universal reference T&&
to allow passing both lvalues and rvalues. This template function is designed to call another function DisplayData()
internally with some additional logic. The expected behavior of the template function PrintLog()
is as follows:
The PrintLog()
function should accept arguments of any type, i.e., both lvalues
and rvalues
. It should internally pass the argument arg
to the function DisplayData()
, while preserving the lvalue
or rvalue
status of the argument arg
.
By “preserving the lvalue
or rvalue
status” : What does this mean?
It means that if an lvalue is passed to PrintLog()
, it should invoke the overloaded version of DisplayData()
that accepts an lvalue reference i.e. DisplayData(int& x)
.
mindmap root(("DisplayData() with lvalue reference")) (("PrintLog(lvalue)"))
Similarly, if an rvalue is passed to PrintLog()
, it should invoke the overloaded version of DisplayData()
that accepts an rvalue reference i.e. DisplayData(int&& x)
.
mindmap root(("DisplayData() with rvalue reference")) (("PrintLog(rvalue)"))
Now, let’s see what the current code does:
int a = 10;
// Calls PrintLog(int&), as `a` is an lvalue
PrintLog(a);
// Calls PrintLog(int&&), as 20 is an rvalue
PrintLog(20);
Output:
Lvalue reference: 10
Lvalue reference: 20
As we can see, when we pass an lvalue to PrintLog()
, it invokes the correct version of DisplayData()
that accepts an lvalue reference.
But when we invoke PrintLog()
with a temporary value, it still invokes the DisplayData()
that accepts an lvalue reference, which is incorrect. We expected it to invoke the DisplayData()
function that accepts an rvalue reference
as an argument, but this did not happen.
The temporary integer 10
(an rvalue) is passed to DisplayData()
, but it’s treated as an lvalue inside the wrapper because arg
is itself an lvalue when accessed within the PrintLog()
function body. Therefore, instead of calling the rvalue reference overload, it calls the lvalue reference overload of DisplayData()
, which is not what we intended.
Using std::forward<>
for Perfect Forwarding in C++
#
To solve this problem, C++11 introduced std::forward<>
. It enables perfect forwarding, where the argument keeps its original lvalue or rvalue status when passed to another function. By using std::forward<>
, we can ensure that the correct overload of DisplayData
is called.
Let’s rewrite our wrapper function PrintLog()
with std::forward<>
:
#include <utility> // for std::forward
template <typename T>
void PrintLog(T&& arg)
{
// Perfectly forwards arg
DisplayData(std::forward<T>(arg));
}
Now, let’s see what the current code does:
int a = 10;
// Calls PrintLog(int&), as `a` is an lvalue
PrintLog(a);
// Calls PrintLog(int&&), as 20 is an rvalue
PrintLog(20);
Output:
Lvalue reference: 10
Rvalue reference: 20
Now, when we call PrintLog(10);
by passing a temporary value, i.e., an rvalue, the std::forward<T>(arg)
preserves the rvalue status of arg
, so it correctly calls the rvalue overload of DisplayData()
, i.e., the overload with an rvalue reference: DisplayData(int&& x)
.
Similarly, when we call PrintLog(a);
by passing an lvalue, the std::forward<T>(arg)
preserves the lvalue status of arg
, so it correctly calls the lvalue overload of DisplayData()
, i.e., the overload with an lvalue reference: DisplayData(int& x)
.
In this way, std::forward<>
is used in Perfect Forwarding to address the issue.
How Does std::forward<>
Work?
#
The std::forward<T>
function works by conditionally casting the argument to an rvalue reference if it was originally an rvalue, or leaving it as an lvalue if it was originally an lvalue. This is achieved using static_cast
internally.
We cannot use std::forward<T>
without explicitly specifying its template argument. Internally, it uses static_cast<T&&>(arg)
for casting.
When we pass an lvalue to
std::forward<T>
, for example,std::forward<T>(a)
wherea
is anint
lvalue:- The template parameter
T
is deduced asint&
. - Internally, the code
std::forward<T>(a)
is converted tostd::static_cast<T&&>(a)
, which translates tostatic_cast<int& &&>(a)
. - In C++,
& &&
collapses to&
. Thus, the final result is an lvalue reference (int&
). - While invoking
DisplayData()
, the lvalue overloadDisplayData(int& x)
is called.
- The template parameter
When we pass an rvalue to
std::forward<T>
, for example,std::forward<T>(20)
where20
is a temporary value (an rvalue):- The template parameter
T
is deduced asint&&
. - Internally, the code
std::forward<T>(20)
is converted tostd::static_cast<T&&>(20)
, which translates tostatic_cast<int&& &&>(20)
. - In C++,
&& &&
collapses to&&
. Thus, the final result is an rvalue reference (int&&
). - While invoking
DisplayData()
, the rvalue overloadDisplayData(int&& x)
is called.
- The template parameter
In essence, std::forward<>
enables conditional casting based on the original type of the argument.
Also remember: When we cast an lvalue or rvalue reference to a rvalue reference, the result is as follows:
- Casting an lvalue reference to a rvalue reference results in an lvalue.
&& && ---> &&
- Casting an rvalue reference to a rvalue reference results in an rvalue.
& && ---> &&
This concept helps std::forward<>
work internally.
Summary #
std::forward<>
enables perfect forwarding by preserving the status of original value (lvalue or rvalue) of arguments in template functions. It is a powerful tool that, combined with universal references, allows C++ code to call the correct overload based on the argument type. By using std::forward<>
, we achieve clean and efficient generic code without sacrificing performance or clarity.