What is std::forward<> in C++?

What is std::forward<> in C++?

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) where a is an int lvalue:

    • The template parameter T is deduced as int&.
    • Internally, the code std::forward<T>(a) is converted to std::static_cast<T&&>(a), which translates to static_cast<int& &&>(a).
    • In C++, & && collapses to &. Thus, the final result is an lvalue reference (int&).
    • While invoking DisplayData(), the lvalue overload DisplayData(int& x) is called.
  • When we pass an rvalue to std::forward<T>, for example, std::forward<T>(20) where 20 is a temporary value (an rvalue):

    • The template parameter T is deduced as int&&.
    • Internally, the code std::forward<T>(20) is converted to std::static_cast<T&&>(20), which translates to static_cast<int&& &&>(20).
    • In C++, && && collapses to &&. Thus, the final result is an rvalue reference (int&&).
    • While invoking DisplayData(), the rvalue overload DisplayData(int&& x) is called.

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.

Author: Varun

A Software Developer with 20 Years of Experience in C/C++.