How Does std::forward Work in C++?

Perfect forwarding in C++ enables a generic wrapper function to accept arguments and pass them along to another target function while preserving their original value category, whether an lvalue or an rvalue, as well as their const qualifiers. Without perfect forwarding, arguments passed by name within a function body always evaluate as lvalues, causing unexpected deep copies instead of efficient moves. C++ achieves this through a combination of universal references (forwarding references), template argument deduction, reference collapsing rules, and the conditional casting mechanism provided by std::forward.

The Core Problem in Generic Code

In generic programming, wrapper functions often accept arguments to construct objects or delegate tasks. Consider a factory pattern or a logging wrapper:

template 
T create(Arg arg) {
    return T(arg);
}

Passing parameters by value can incur expensive copies. If you instead pass by const Arg&, temporary objects and rvalues cannot be moved, and non-const lvalues cannot be modified. Overloading for every possible permutation of const and non-const references quickly leads to an exponential explosion of code when a function accepts multiple arguments.

Furthermore, even if an rvalue reference like Arg&& is accepted, the named parameter inside the function body itself is an lvalue. If passed directly to another function, it resolves to an lvalue overload, preventing move semantics unless cast explicitly.

Forwarding References and Type Deduction

To solve this, C++11 introduced forwarding references (also called universal references). A forwarding reference takes the form T&&, where T is a deduced template parameter for that specific function call:

template 
void wrapper(T&& arg);

When an argument is passed to wrapper(T&&):

Reference Collapsing Rules

Because C++ does not allow references to references directly in user code, the compiler applies reference collapsing during template instantiation according to strict rules:

Applying these rules to the wrapper:

This deduction ensures the function signature adapts dynamically to preserve the argument's reference type.

How std::forward Works

While the deduced type retains knowledge of whether the caller supplied an lvalue or an rvalue, the parameter arg inside the function body is an entity with a name. By definition in C++, any expression consisting solely of a named variable has an lvalue value category.

std::forward restores the original value category through a conditional static cast. Its standard implementation resembles the following:

template 
constexpr T&& forward(std::remove_reference_t& param) noexcept {
    return static_cast(param);
}

template 
constexpr T&& forward(std::remove_reference_t&& param) noexcept {
    static_assert(!std::is_lvalue_reference_v,
                  "Template argument must not be an lvalue reference for rvalue overload.");
    return static_cast(param);
}

Case 1: An Lvalue Was Passed to the Wrapper

  1. Caller passes an lvalue w of type Widget.
  2. T is deduced as Widget&.
  3. The wrapper receives Widget& arg.
  4. Inside the wrapper, std::forward(arg) is called.
  5. Inside std::forward, the return type is Widget& &&, which collapses to Widget&.
  6. static_cast(arg) returns an lvalue reference, preserving lvalue semantics.

Case 2: An Rvalue Was Passed to the Wrapper

  1. Caller passes a temporary or moved value Widget{}.
  2. T is deduced as Widget.
  3. The wrapper receives Widget&& arg.
  4. Inside the wrapper, std::forward(arg) is called.
  5. Inside std::forward, the return type is Widget&&.
  6. static_cast(arg) casts the named lvalue arg back into an rvalue expression (xvalue), allowing downstream functions to move it.

Practical Implementation Example

A standard application of perfect forwarding is in functions like std::make_unique or container emplace operations:

#include 
#include 
#include 

class Resource {
public:
    Resource(const std::string& name) {
        std::cout << "Copied name: " << name << '\n';
    }
    Resource(std::string&& name) {
        std::cout << "Moved name: " << name << '\n';
    }
};

template 
std::unique_ptr make_resource(Args&&... args) {
    return std::unique_ptr(new T(std::forward(args)...));
}

int main() {
    std::string text = "Persistent";
    
    // Calls the copy constructor because 'text' is an lvalue
    auto r1 = make_resource(text);
    
    // Calls the move constructor because a temporary is an rvalue
    auto r2 = make_resource(std::string("Temporary"));
}

By encoding the argument's category into the template parameter Args and using std::forward(args)..., each argument arrives at the Resource constructor in its original state without unnecessary allocations or duplicate wrapper overloads.