Skip to content
C++
Idiom
since C++11
Intermediate

Perfect Forwarding

Preserve value categories through template wrappers using forwarding references, std::forward, and reference collapsing.

Perfect Forwardingsince C++11

A technique using forwarding references (T&& in a deduced context) and std::forward to propagate the value category β€” lvalue or rvalue β€” of arguments unchanged through a template wrapper to a downstream function.

Overview

When a function template receives arguments and passes them elsewhere, the naive approach silently converts rvalue arguments to lvalues β€” costing moves that become copies. Perfect forwarding solves this by preserving value category through the entire call chain.

The mechanism rests on two C++11 rules working in concert: forwarding references (a T&& parameter where T is a deduced template parameter) and reference collapsing (the rules for merging double references). std::forward<T> then uses the deduced T to decide whether to cast back to an rvalue or leave as an lvalue.

Why Value Category Gets Lost

Named variables are always lvalues, even if they hold rvalue references:

cpp
void sink(std::string&& s);    // only binds rvalues

void wrapper(std::string&& s) {
    sink(s);              // error: s is an lvalue β€” it has a name
    sink(std::move(s));   // compiles, but unconditionally moves β€” wrong for generics
}

A generic wrapper must handle both lvalue and rvalue inputs and forward each appropriately. That is exactly what perfect forwarding provides.


Syntax

Forwarding References

T&& is a forwarding reference only when T is a directly deduced template parameter:

cpp
template<typename T>
void f(T&& x);               // forwarding reference β€” T is deduced from the call

void g(std::string&& x);     // rvalue reference β€” no deduction

template<typename T>
void h(std::vector<T>&& x);  // rvalue reference β€” T deduced, but parameter is
                              // std::vector<T>&&, not a plain T&&

auto&& is also a forwarding reference wherever auto is deduced (C++11 range-for, C++14 generic lambdas, C++20 abbreviated templates):

cpp
for (auto&& elem : container) { /* elem is a forwarding ref */ }  // C++11

auto fn = [](auto&& x) { /* x is a forwarding ref */ };           // C++14

Reference Collapsing (C++11)

When T is deduced as a reference type, double-references are collapsed:

T deduced asParameter T&& collapses to
int&int&
const int&const int&
int (non-reference)int&&

The rule is simple: any combination containing at least one & collapses to &. Only && + && stays &&.

How std::forward Works

cpp
// Conceptual implementation (C++11, simplified)
template<typename T>
T&& forward(std::remove_reference_t<T>& arg) noexcept {
    return static_cast<T&&>(arg);
}

When T = int&: static_cast<int& &&> β†’ static_cast<int&> β€” a no-op, returns lvalue.
When T = int: static_cast<int&&> β€” restores rvalue-ness to the named variable.

Always specify T explicitly. std::forward with a deduced argument type is almost always wrong. std::forward needs the original deduced T from the outer function, not the type of the expression it receives.


Examples

Generic Factory

cpp
template<typename T, typename... Args>
T make(Args&&... args) {
    return T(std::forward<Args>(args)...);
}

struct Widget {
    Widget(const std::string& name, int id);  // lvalue overload
    Widget(std::string&& name, int id);       // rvalue overload
};

std::string name = "foo";
auto w1 = make<Widget>(name, 1);             // lvalue: copies name
auto w2 = make<Widget>(std::move(name), 2);  // rvalue: moves name
auto w3 = make<Widget>("bar", 3);            // rvalue: constructed in-place

This is the exact pattern behind std::make_unique, std::make_shared (both C++14), and every container's emplace family.

Wrapping Callables

Both the callable and its arguments must be forwarded:

cpp
// C++14: decltype(auto) preserves reference return types
template<typename F, typename... Args>
decltype(auto) invoke_logged(F&& f, Args&&... args) {
    std::clog << "invoking\n";
    return std::forward<F>(f)(std::forward<Args>(args)...);
}

Using plain auto return type instead of decltype(auto) would strip references and cv-qualifiers β€” a subtle correctness bug in wrappers.

LIFT: Wrapping Overload Sets (C++14)

You cannot pass an overloaded function name directly to a higher-order function; the compiler has no argument types to select an overload. A forwarding lambda wraps the entire overload set:

cpp
#define LIFT(name)                                                     \
    [](auto&&... x)                                                    \
    noexcept(noexcept(name(std::forward<decltype(x)>(x)...)))          \
    -> decltype(name(std::forward<decltype(x)>(x)...))                 \
    { return name(std::forward<decltype(x)>(x)...); }

// std::transform(v.begin(), v.end(), out, foo);  // error if foo is overloaded
std::transform(v.begin(), v.end(), out, LIFT(foo));  // ok

The noexcept specifier and explicit return type ensure the lambda doesn't accidentally weaken exception guarantees or strip reference returns.

std::forward_as_tuple for Deferred Construction (C++11)

When an argument pack must cross a boundary before the target object is constructed:

cpp
template<typename... Args>
void maybe_emplace(std::vector<Widget>& v, bool condition, Args&&... args) {
    if (condition) {
        v.emplace_back(std::forward<Args>(args)...);
    } else {
        // capture pack as tuple of references; rvalues become rvalue refs
        auto pack = std::forward_as_tuple(std::forward<Args>(args)...);
        // ... later:
        std::apply([&v](auto&&... a) {
            v.emplace_back(std::forward<decltype(a)>(a)...);
        }, std::move(pack));
    }
}

std::forward_as_tuple builds a std::tuple of references, preserving value category. Elements that were rvalues become T&& members in the tuple.

C++20: Abbreviated Templates

With abbreviated function templates, T is not named β€” use decltype(x) instead:

cpp
// C++20 abbreviated template
decltype(auto) wrap(auto&& f, auto&&... args) {
    return std::forward<decltype(f)>(f)(
        std::forward<decltype(args)>(args)...
    );
}

// Generic lambda (C++14+)
auto wrap = [](auto&& f, auto&&... args) -> decltype(auto) {
    return std::forward<decltype(f)>(f)(
        std::forward<decltype(args)>(args)...
    );
};

emplace vs push

emplace_back uses perfect forwarding to construct directly inside the container, eliminating the intermediate temporary:

cpp
std::vector<std::string> v;
std::string s = "hello";

v.push_back(s);            // copy-constructs std::string, then copy into slot
v.push_back(std::move(s)); // move-constructs std::string, then move into slot
v.push_back("world");      // constructs temporary std::string, then moves it

v.emplace_back("world");   // forwards "world" directly to std::string(const char*)
v.emplace_back(5, 'x');    // calls std::string(size_t, char) β€” no push_back equivalent

For types heavier than std::string, or types with deleted move constructors, emplace_back can be the only viable option.


Best Practices

Return decltype(auto) from wrappers. Plain auto strips references and cv-qualifiers from the return type β€” a transparent wrapper must use decltype(auto) to propagate them.

Constrain forwarding references. An unconstrained T&& accepts every type, including arguments intended for other overloads. In C++20, use concepts:

cpp
// C++20: constrained forwarding ref
template<std::derived_from<Widget> T>
void process(T&& x);

// C++11/14: enable_if
template<typename T, typename = std::enable_if_t<std::is_base_of_v<Widget, std::decay_t<T>>>>
void process(T&& x);

Without constraints, a forwarding-reference constructor or function beats the copy constructor for non-const lvalues β€” a common surprise in class templates.

Forward callables. A functor may have reference-qualified operator() overloads. Always forward the callable itself, not just its arguments.


Common Pitfalls

Forwarding the Same Argument Twice

After std::forward<T>(x) where T is a non-reference, x is in a moved-from state. A second std::forward<T>(x) is undefined behavior:

cpp
template<typename T>
void bad(T&& x) {
    log(std::forward<T>(x));   // x may be moved here
    use(std::forward<T>(x));   // UB when T is a non-reference type
}

template<typename T>
void good(T&& x) {
    log(x);                    // inspect as lvalue β€” no move
    use(std::forward<T>(x));   // forward only at the final consumption point
}

std::move on a Forwarding Reference

std::move unconditionally casts to rvalue. Applied to a forwarding reference bound to an lvalue, it silently steals the caller's value:

cpp
template<typename T>
void bad(T&& x) {
    sink(std::move(x));       // moves even if x was an lvalue β€” caller loses its object
}

template<typename T>
void good(T&& x) {
    sink(std::forward<T>(x)); // moves only if x was originally an rvalue
}

Braced-Init-Lists Cannot Deduce

T&& does not help with brace initialization β€” the compiler has no type to deduce T from:

cpp
template<typename T>
void f(T&& x);

f({1, 2, 3});                      // error: cannot deduce T
f(std::vector<int>{1, 2, 3});      // ok: T deduces to std::vector<int>

Forwarding-Reference Constructors Hijack Copy

A templated constructor with T&& is a better match than the copy constructor for non-const lvalues:

cpp
struct S {
    template<typename T>
    S(T&& x) { /* ... */ }  // matches S& better than S(const S&)
};

S a;
S b(a);  // calls template constructor with T=S&, not copy constructor!

Constrain the template parameter to exclude S and its cv/ref variations, or add enable_if/concept guards.


See Also

  • Move Semantics β€” the value category model that perfect forwarding preserves
  • Variadic Templates β€” required for forwarding argument packs with Args&&...
  • Type Erasure β€” forwarding is frequently used inside type-erased wrappers
  • CRTP β€” policy-based designs that pair forwarding references with static dispatch
  • Deducing This β€” C++23 explicit object parameters interact with forwarding reference semantics