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.


std::forward_like (C++23)

std::forward_like transfers the value category of one expression onto another type. It is useful when a wrapper owns a member and wants to forward it with the same value category as *this β€” something std::forward alone cannot express.

cpp
// C++23
#include <utility>

template<typename Self, typename T>
decltype(auto) forward_member(Self&& self, T& member) {
    return std::forward_like<Self>(member);
    // If Self is an rvalue ref β†’ casts member to T&&
    // If Self is an lvalue ref β†’ returns member as T&
}

struct Wrapper {
    std::string data;

    // Propagate the wrapper's own value category to the member
    decltype(auto) get(this auto&& self) {
        return std::forward_like<decltype(self)>(self.data);
    }
};

Wrapper w{"hello"};
std::string a = w.get();               // lvalue Wrapper  β†’ copies data
std::string b = std::move(w).get();    // rvalue Wrapper  β†’ moves data

std::forward_like is not a drop-in for std::forward. It ignores the actual type of member; it only looks at the value category of the Self template argument. Pair it with deducing-this (this auto&&) for the most natural ownership-propagation pattern.


Forwarding in variadic templates

Variadic forwarding is the backbone of every generic factory and dispatcher in the standard library. The Args&&... pattern combined with std::forward<Args>(args)... expands both the pack and the forward in a single expression.

cpp
// C++11
template<typename... Args>
void relay(Args&&... args) {
    target(std::forward<Args>(args)...);
    //     ^β€”β€” expands to: forward<A0>(a0), forward<A1>(a1), ...
}

When the pack must be passed to multiple destinations, capture it first with std::forward_as_tuple to avoid forwarding a moved-from argument:

cpp
// C++11
template<typename... Args>
void dual_dispatch(Args&&... args) {
    auto pack = std::forward_as_tuple(std::forward<Args>(args)...);

    std::apply([](auto&&... a) { logger(std::forward<decltype(a)>(a)...); }, pack);
    std::apply([](auto&&... a) { target(std::forward<decltype(a)>(a)...); }, std::move(pack));
}

Moving pack on the second std::apply call ensures the stored rvalue references are forwarded to target rather than left in an indeterminate state.


Forwarding in emplace operations

emplace methods use perfect forwarding to construct objects directly inside a container slot, avoiding all intermediate temporaries. The container calls std::allocator_traits::construct, which forwards the argument pack to the element's constructor.

cpp
// C++11
#include <vector>
#include <map>

struct Point { int x, y, z; };

std::vector<Point> pts;
pts.emplace_back(1, 2, 3);       // constructs Point{1,2,3} in-place β€” zero copies

std::map<int, std::string> m;
m.emplace(42, "hello");          // constructs pair<const int, string> in-place
m.try_emplace(42, 5, 'x');       // C++17: only constructs if key absent

try_emplace (C++17) has an important advantage over emplace: it does not construct the mapped value when the key already exists, so forwarded arguments are never consumed uselessly:

cpp
// C++17
std::map<int, std::vector<int>> cache;
cache.try_emplace(key, std::move(expensive_vec));
// If key exists, expensive_vec is NOT moved β€” it remains valid

Forwarding constructors

A forwarding constructor delegates to another constructor while preserving value categories, enabling efficient wrapper types. The pattern is common in optional-like, expected, and tagged-union types.

cpp
// C++11
template<typename T>
class Box {
    T value_;
public:
    // Forwarding constructor: accepts any argument(s) constructible into T
    template<typename... Args,
             typename = std::enable_if_t<std::is_constructible_v<T, Args...>>>
    explicit Box(Args&&... args)
        : value_(std::forward<Args>(args)...) {}
};

Box<std::string> b1("hello");           // T constructed from const char*
Box<std::string> b2(5, 'x');            // T constructed from (size_t, char)

std::string s = "world";
Box<std::string> b3(s);                 // copies s into value_
Box<std::string> b4(std::move(s));      // moves s into value_

In C++20, replace enable_if with a concept constraint for cleaner diagnostics:

cpp
// C++20
template<typename T>
class Box {
    T value_;
public:
    template<typename... Args>
        requires std::constructible_from<T, Args...>
    explicit Box(Args&&... args)
        : value_(std::forward<Args>(args)...) {}
};

Guard against accidentally matching copy/move construction of Box itself by adding !std::same_as<std::decay_t<FirstArg>, Box> to the constraint.


Common pitfall: forwarding after use

Once an argument has been forwarded as an rvalue, the source object is in a valid-but-unspecified state β€” any subsequent access or second forward is undefined behaviour or a logical bug. This pitfall is particularly subtle when a forwarded argument also appears in a default argument or a side-effectful expression.

cpp
// C++11 β€” WRONG
template<typename T>
void wrong(T&& x) {
    log(std::forward<T>(x));    // may move x into log
    store(std::forward<T>(x));  // UB: x may already be moved-from
}

// CORRECT: inspect first, forward last
template<typename T>
void right(T&& x) {
    log(x);                     // read as lvalue β€” no move
    store(std::forward<T>(x));  // safe: single consumption point
}

The problem also appears across conditional branches:

cpp
// C++11 β€” WRONG
template<typename T>
void conditional_wrong(T&& x, bool flag) {
    auto& ref = some_map[std::forward<T>(x)];   // may move x
    if (flag) sink(std::forward<T>(x));          // double-forward
}

// CORRECT: forward at most once per code path
template<typename T>
void conditional_right(T&& x, bool flag) {
    auto key = x;                                // copy for the lookup
    auto& ref = some_map[key];
    if (flag) sink(std::forward<T>(x));          // single forward
}

Perfect forwarding in lambdas (C++20)

C++20 template lambdas allow explicit template parameter lists, making it possible to capture the deduced T and call std::forward<T> correctly inside a lambda β€” previously impossible without a macro.

cpp
// C++14: decltype workaround β€” verbose but correct
auto wrap14 = [](auto&& x) -> decltype(auto) {
    return sink(std::forward<decltype(x)>(x));
};

// C++20: explicit template parameter in lambda
auto wrap20 = []<typename T>(T&& x) -> decltype(auto) {
    return sink(std::forward<T>(x));   // T is now in scope
};

// C++20: variadic template lambda
auto relay20 = []<typename... Args>(Args&&... args) {
    target(std::forward<Args>(args)...);
};

The C++20 form also enables constraints directly on the lambda's template parameter:

cpp
// C++20
auto process = []<std::integral T>(T&& val) {
    // only accepts integral types, forwarding preserved
    consume(std::forward<T>(val));
};

Abbreviated forwarding with auto&& (C++20)

auto&& as a function parameter (abbreviated template syntax, C++20) creates a forwarding reference without naming the template parameter, but std::forward still requires a type β€” use decltype(param) to recover it.

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

This is equivalent to the explicit template form but considerably more concise. The same pattern applies in abbreviated free functions, member functions with this auto&& (deducing-this), and immediately invoked lambdas:

cpp
// C++20: abbreviated member function
struct Logger {
    decltype(auto) call(this auto&& self, auto&&... args) {
        self.log_call();
        return std::forward<decltype(self)>(self).impl_(
            std::forward<decltype(args)>(args)...
        );
    }
private:
    std::function<void()> impl_;
};

Never write std::forward<auto>(x). The keyword auto is not a valid template argument for std::forward; always spell out decltype(x) to extract the deduced type.


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