Before discussing perfect forwarding in C++, we need to first understand why perfect forwarding and std::forward
were introduced in C++11. Once we understand the problem, we will see how perfect forwarding helps solve it. So, let’s start.
Problem of Preserving Rvalue Status #
Suppose we have three overloaded versions of a function, such as processData()
.
- The first overloaded version accepts an lvalue reference as an argument and prints the value.
void processData(int& x)
{
std::cout << "Lvalue reference: " << x << std::endl;
}
- The second overloaded version accepts a
const
lvalue reference and prints the content.
void processData(const int& x)
{
std::cout << "Const Lvalue reference: " << x << std::endl;
}
- Finally, the third overloaded version of the same function
processData()
accepts anrvalue reference
.
void processData(int&& x)
{
std::cout << "Rvalue reference: " << x << std::endl;
}
These are the three overloaded functions. Now, consider that we need to implement a wrapper function. The purpose of this wrapper()
function is to do some extra work but internally call one of the overloaded version of processData() function.
For example, if the wrapper()
function is called with an lvalue, it should internally call the version of processData
that accepts an lvalue reference i.e. processData(int& x)
mindmap root(("processData() with lvalue reference")) (("wrapper(lvalue)"))
Similarly, if the wrapper function is called with a const
lvalue, it should call the version of processData()
that accepts a const
lvalue reference i.e. processData(const int& x)
mindmap root(("processData() with const lvalue reference")) (("wrapper(const lvalue)"))
If the wrapper function is called with a temporary value (an rvalue), it should call the version of processData()
that accepts an rvalue reference processData(int&& x)
mindmap root(("processData() with rvalue reference")) (("wrapper(rvalue)"))
So, we want the wrapper function to remain the same, but internally it should call the correct overloaded version of processData()
based on the type of the parameter it receives. Now, this is a perfect case for a template function, so we create a template wrapper() function like this:
template<typename T>
void wrapper(T& arg)
{
processData(arg);
}
Now we will try to invoke the wrapper()
with three different types of values i.e. lvalue, const lvalue, and rvalue. Like this,
int a = 10;
// Calls wrapper(int&), as `a` is an lvalue
wrapper(a);
const int value = 23;
// Calls wrapper(const int&), as `value` is a const lvalue
wrapper(value);
// Calls wrapper(int&&), as 20 is an rvalue
wrapper(20); // Compile Error
Output:
example13.cpp: In function ‘int main()’:
example13.cpp:38:13: error: cannot bind non-const lvalue reference of type ‘int&’ to an rvalue of type ‘int’
38 | wrapper(20);
| ^~
example13.cpp:20:17: note: initializing argument 1 of ‘void wrapper(T&) [with T = int]’
20 | void wrapper(T& arg)
| ~~~^~~
This works fine for lvalues and const
lvalues i.e. wrapper(a)
will invoke the overloaded version of processData()
that accepts a lvalue reference and wrapper(value);
will invoke the version of processData()
that accepts a const lvalue reference.
However, if we try to pass a temporary value (e.g., 20
) to the wrapper function like wrapper(20);
, it will result in a compile-time error because a temporary value (an rvalue) cannot bind to a non-const lvalue reference.
Now to fix this issue we can try different things,
First Fix Attempt: Accepting rvalue references #
We can modify the template function to accept rvalue references like this:
template<typename T>
void wrapper(T&& arg)
{
processData(arg);
}
Now we will try to invoke the wrapper()
with three different types of values i.e. lvalue, const lvalue, and rvalue. Like this,
int a = 10;
// Calls wrapper(int&), as `a` is an lvalue
wrapper(a);
const int value = 23;
// Calls wrapper(const int&), as `value` is a const lvalue
wrapper(value);
// Calls wrapper(int&&), as 20 is an rvalue
wrapper(20); // Compile Error
Output:
Lvalue reference: 10
Const Lvalue reference: 23
Lvalue reference: 20
Now, there is no compile error when we pass an rvalue to the wrapper function. However, this implementation has a flaw: the rvalue’s status is not preserved. When arg
is forwarded to processData()
, it becomes an lvalue inside the wrapper()
, and the lvalue overload of processData()
is called instead of the rvalue overload. For example:
wrapper(30);
It calls the lvalue reference version, not the rvalue reference version.
So now, there was no way to modify our template function to automatically invoke the correct function internally based on the type of reference it receives. If it receives an lvalue reference, it should invoke the processData()
function that accepts an lvalue reference. If it receives a const lvalue reference, it should invoke the processData
function that accepts a const lvalue reference. Similarly, if it receives an rvalue, it should invoke the processData
function that accepts an rvalue reference. However, this was not happening in both cases.
Final Fix: Using std::forward<>
for Perfect Forwarding
#
To address this, Perfect Forwarding were introduced. Using std::forward<T>()
in templates we restore the original lvalue or rvalue status of a variable. We can call this in the wrapper() function like this,
template<typename T>
void wrapper(T&& arg)
{
processData(std::forward<T>(arg));
}
Now, inside the wrapper()
function, while invoking the processData()
function, we will first call std::forward<T>(arg)
on the argument before passing it to the processData()
function.
std::forward<T>(arg)
allows us to forward arguments to another function while preserving the argument’s lvalue or rvalue status. This enables conditional casting, which restores the exact type of the argument, whether it was initially an lvalue or rvalue.
For instance, if we pass an rvalue to the wrapper function, std::forward<T>()
will preserve its rvalue status before invoking processData()
. Similarly, if we pass an lvalue, std::forward<T>()
will retain its lvalue status before invoking processData()
. This ensures that processData()
is always called with the correct type of reference.
For example:
int a = 10;
// Calls wrapper(int&), as `a` is an lvalue
wrapper(a);
const int value = 23;
// Calls wrapper(const int&), as `value` is a const lvalue
wrapper(value);
// Calls wrapper(int&&), as 20 is an rvalue
wrapper(20);
Output:
Lvalue reference: 10
Const Lvalue reference: 23
Rvalue reference: 20
std::forward<T>(arg)
allows us to preserve the “value category” (lvalue or rvalue) of the argument when forwarding it to another function. In the above example,
- When we invoked the
wrapper()
with a lvalue likewrapper(a)
in above example. Thestd::forward<T>(arg)
preserved itslvalue
status and the overloaded version ofprocessData()
was invoked that accepts alvalue reference
. - When we invoked the
wrapper()
with a const lvalue likewrapper(value)
in above example. Thestd::forward<T>(arg)
preserved itsconst lvalue
status and the overloaded version ofprocessData()
was invoked that accepts aconst lvalue reference
. - When we invoked the
wrapper()
with a rvalue likewrapper(20)
in above example. Thestd::forward<T>(arg)
preserved itsrvalue
status and the overloaded version ofprocessData()
was invoked that accepts arvalue reference
.
Now, std::forward<T>(arg)
is similar to static_cast<T&&>(arg)
. So, instead of std::forward
, we could use static_cast
as well like this,
template<typename T>
void wrapper(T&& arg)
{
processData(static_cast<T&&>(arg));
}
It wil have same effect as using std::forward<T>(arg)
. However, std::forward
more clearly indicates our intention.
How std::forward<T>(arg)
Works?
#
How do static_cast<T&&>(arg)
or std::forward<T>(arg)
help restore the original status of the argument?
When we cast the lvalue or rvalue reference to a rvalue reference, the result is as follows:
- If the original value was an rvalue, the result remains an rvalue i.e.
&& && ---> &&
- If the original was an lvalue, casting to rvalue leaves it as an lvalue.
& && ---> &&
This behavior in C++ allows us to use static_cast<T&&>(arg)
or std::forward<T>(arg)
to preserve the original status of a variable before passing it to the next function. This is called perfect forwarding, it means forwarding an argument to the next function while preserving its original lvalue or rvalue status.
Summary #
Perfect forwarding ensures that arguments are forwarded to the target function with their original value category intact, whether they are lvalues or rvalues. It is a crucial technique in modern C++ for writing generic and efficient code.