Skip to content
C++
C++11Intermediate

Perfect Forwarding with std::forward

When you write a wrapper function that passes arguments through to another function, a problem emerges: inside the wrapper, all parameters are lvalues (they have names), so any rvalue-ness from the caller is silently lost. Perfect forwarding is the technique — using forwarding references and std::forward — that preserves the original value category all the way through to the eventual function call. It is what makes std::make_unique, std::vector::emplace_back, and similar factory utilities work efficiently for both lvalues and rvalues.

The forwarding problem

A named parameter is always an lvalue inside a function body — even if the caller passed an rvalue. This means a naive wrapper always triggers the copy path, never the move path, even when the caller intended a move.

void process(std::string& s)  { std::cout << "lvalue path\n"; }
void process(std::string&& s) { std::cout << "rvalue path\n"; }

// Naive wrapper — always lvalue, rvalue-ness is lost:
void wrapper(std::string s) {
    process(s);      // s has a name → lvalue → always calls lvalue overload
}

std::string name = "Alice";
wrapper(name);            // prints: lvalue path (correct)
wrapper(std::move(name)); // ALSO prints: lvalue path (WRONG — move lost!)

Forwarding references (T&&)

When a function template parameter is written as T&& where T is a deduced template parameter, it is not an rvalue reference — it is a forwarding reference (also called a universal reference). It can bind to both lvalues and rvalues, and reference collapsing rules ensure that T is deduced as Widget& for lvalues and Widget for rvalues.

// T&& in a template = forwarding reference (NOT an rvalue reference):
template<typename T>
void wrapper(T&& arg) {
    // T is deduced as:
    //   Widget&  when called with an lvalue  → T&& collapses to Widget&
    //   Widget   when called with an rvalue  → T&& stays Widget&&
    process(arg);  // still wrong: arg has a name → always lvalue
}

// Reference collapsing rules:
// T = Widget&   → T&& = Widget& && = Widget&   (lvalue ref)
// T = Widget    → T&& = Widget&&               (rvalue ref)

std::forward — conditional cast

std::forward<T>(arg) is a conditional cast: if T was deduced as an lvalue reference type (Widget&), it returns arg as an lvalue; if T was deduced as a non-reference type (Widget), it returns arg as an rvalue (xvalue). This restores the value category the caller originally had.

template<typename T>
void wrapper(T&& arg) {
    process(std::forward<T>(arg));  // preserves value category!
}

std::string name = "Alice";
wrapper(name);            // T = string& → forward returns lvalue → lvalue path
wrapper(std::move(name)); // T = string  → forward returns rvalue → rvalue path
wrapper("Bob");           // T = string  → forward returns rvalue → rvalue path

Always write std::forward<T>(arg) with the explicit template argument — the compiler cannot deduce it. And always forward a parameter at most once: forwarding moves from it (if rvalue), so it should not be used again after.

Practical use: factory functions

Perfect forwarding enables factory utilities like std::make_unique and emplace_back to construct objects in place using whatever arguments the caller provides, preserving their value categories for maximum efficiency.

// Simplified make_unique:
template<typename T, typename... Args>
std::unique_ptr<T> my_make_unique(Args&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

struct Widget {
    Widget(std::string name, int value);
};

std::string n = "foo";
auto w = my_make_unique<Widget>(n, 42);
//   n is lvalue → forwarded as lvalue → string copy constructor called for 'name'

auto w2 = my_make_unique<Widget>(std::move(n), 42);
//   n is xvalue → forwarded as rvalue → string move constructor called for 'name'

Key rules to remember

T&& in a template parameter is a forwarding reference, not an rvalue reference

It binds to both lvalues and rvalues. The distinction from a plain rvalue reference (Widget&&) is that T is deduced.

std::forward<T> restores the value category — always pass the template parameter explicitly

std::forward<T>(arg) is a conditional cast: lvalue if T is an lvalue ref type, rvalue otherwise. Without the <T>, it cannot work correctly.

Only forward a parameter once

After std::forward<T>(arg), arg may have been moved from. Using it again is undefined behavior for rvalues.

Use std::move for unconditional 'I'm done with this'; use std::forward for 'pass this along as-is'

move is for transferring out of your scope. forward is for relay — passing through to another function while preserving the original caller's intent.

Sign in to track progress