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

Perfect Forwarding

Forwarding references and std::forward preserve the value category of arguments through template wrappers — enabling zero-overhead generic factories and emplace-style APIs.

Perfect Forwardingsince C++11

A technique using forwarding references (T&& in a deduced context) and std::forward<T> to pass arguments to another function while preserving their original value category — lvalue stays lvalue, rvalue stays rvalue.

Overview

Without perfect forwarding, wrapping a function call in a template requires separate overloads for lvalue and rvalue arguments, which explodes combinatorially with multiple parameters. Perfect forwarding solves this with two cooperating mechanisms:

Forwarding references — a parameter declared T&& where T is a deduced template parameter. Unlike a plain rvalue reference, a forwarding reference binds to both lvalues and rvalues:

cpp
template<typename T>
void wrapper(T&& arg);   // forwarding reference — T is deduced

void f(int&& x);         // plain rvalue reference — not a forwarding reference

Reference collapsing — the rule that resolves double-references produced by template substitution:

Declared asDeduced TResult
T&&int& (from lvalue)int& && → int&
T&&int (from rvalue)int&&

So when you pass an lvalue int x, T deduces to int&, giving int& && which collapses to int&. When you pass rvalue 42, T deduces to int, giving int&&.

std::forward<T> — restores the original value category. It is a conditional cast: if T is an lvalue reference type, it casts to lvalue; otherwise it casts to rvalue.

cpp
template<typename T>
void forward_to_sink(T&& arg) {
    sink(std::forward<T>(arg));   // preserves lvalue/rvalue-ness of arg
}

Syntax

cpp
#include <utility>   // std::forward

// Single-argument factory
template<typename T, typename Arg>
T make(Arg&& arg) {
    return T(std::forward<Arg>(arg));
}

// Variadic factory (emplace pattern)
template<typename T, typename... Args>
T construct(Args&&... args) {
    return T(std::forward<Args>(args)...);
}

// std::make_unique / std::make_shared use exactly this pattern
auto p = std::make_unique<Widget>(std::move(heavy), 42);

How std::forward Works

std::forward<T>(arg) is equivalent to static_cast<T&&>(arg) with one additional constraint: the template argument must be explicit — std::forward without <T> does not compile.

cpp
template<typename T>
void f(T&& x) {
    // These three are equivalent:
    g(std::forward<T>(x));
    g(static_cast<T&&>(x));

    // WRONG — move unconditionally converts to rvalue:
    g(std::move(x));   // always rvalue, loses lvalue information
}

Common Patterns

Factory / emplace constructor

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

Forwarding lambda wrapper

cpp
auto logged = [](auto&& fn, auto&&... args) {
    log("calling");
    return fn(std::forward<decltype(args)>(args)...);
};

std::invoke-compatible wrapper

cpp
template<typename F, typename... Args>
decltype(auto) timed(F&& f, Args&&... args) {
    auto t0 = std::chrono::steady_clock::now();
    decltype(auto) result = std::invoke(std::forward<F>(f),
                                        std::forward<Args>(args)...);
    log_elapsed(t0);
    return result;
}

Common Pitfalls

Forwarding twice

std::forward may move out of the argument. Forwarding twice is undefined behaviour if the first forward moved:

cpp
template<typename T>
void bad(T&& x) {
    a(std::forward<T>(x));   // may move x
    b(std::forward<T>(x));   // UB if x was moved from
}

Forward a given argument only once.

Using auto&& without decltype

In lambdas with auto&& parameters, you need decltype to recover the type:

cpp
auto f = [](auto&& x) {
    g(std::forward<decltype(x)>(x));   // correct
    // g(std::forward<auto>(x));       // C++23 only
};

Confusing forwarding reference with rvalue reference

T&& is a forwarding reference only when T is deduced. In a class template member, T is already fixed:

cpp
template<typename T>
struct Wrapper {
    void set(T&& x);    // plain rvalue reference — T is fixed
    template<typename U>
    void set2(U&& x);   // forwarding reference — U is deduced
};

Forwarding Variadic Packs

When a function accepts a parameter pack Args&&... args, each element must be individually forwarded by expanding both the pack and std::forward together:

cpp
template<typename... Args>
void relay(Args&&... args) {
    target(std::forward<Args>(args)...);   // expands to: target(fwd(a0), fwd(a1), ...)
}

The ... after std::forward<Args>(args) is a pack expansion — the compiler expands the pattern std::forward<Args>(args) once per element, zipping Args and args in lockstep.

Fold expressions with forwarded packs

C++17 fold expressions let you apply an operator across a pack. Combine them with std::forward when the operation must honour value categories:

cpp
// Print all arguments, forwarding each to an overloaded sink
template<typename... Args>
void print_all(Args&&... args) {
    (sink(std::forward<Args>(args)), ...);   // unary right-fold over comma operator
}

Note: fold expressions expand eagerly left-to-right (for (..., expr)) or right-to-left (for (expr, ...)). Because each element is forwarded exactly once this is safe even when the sink moves from rvalues.

Generic lambda forwarding pack (C++20)

C++20 allows auto&& parameter packs in lambdas. The correct idiom uses decltype to recover each argument's exact type:

cpp
// C++20: variadic generic lambda that perfectly forwards all arguments
auto relay = [](auto&&... args) {
    return target(std::forward<decltype(args)>(args)...);
};

// C++23 shorthand — std::forward<auto> is valid
auto relay23 = [](auto&&... args) {
    return target(std::forward<decltype(args)>(args)...);
};

std::forward<decltype(x)>(x) is the canonical idiom whenever T is not directly available — for instance inside a generic lambda or an if constexpr branch. decltype(x) preserves the reference qualification that reference collapsing already encoded in x's type.

Constrained Forwarding References (C++20)

C++20 concepts let you write constraints on template parameters, but applying a concept directly to a forwarding reference has a non-obvious pitfall.

The pitfall: concepts see the reference type

When you write:

cpp
template<std::integral I>
void foo(I&& i);   // WRONG intent — I deduces to int& for lvalues

Passing an lvalue int x deduces I as int&. Most standard concepts (std::integral, std::same_as<int>, etc.) are defined on non-reference types and will reject int&, so the constrained overload silently loses to an unconstrained one:

cpp
template<std::integral I>
void foo(I&& i) { puts("integral"); }   // selected for rvalues only

template<typename T>
void foo(T&& t) { puts("other"); }      // selected for lvalues — surprising!

int x = 0;
foo(0);   // prints "integral"
foo(x);   // prints "other"  — not what you wanted

The fix: strip the reference in the constraint

Use std::remove_cvref_t to make the concept test the underlying type:

cpp
template<typename T>
    requires std::integral<std::remove_cvref_t<T>>
void foo(T&& i) {
    // works for both lvalue int and rvalue int
}

Or wrap it in a helper concept:

cpp
template<typename T, typename U>
concept decays_to = std::same_as<std::remove_cvref_t<T>, U>;

template<typename T>
    requires decays_to<T, int>
void foo(T&& x);

Ranges-style: concept that ignores ref-qualification

C++20 Ranges uses a different approach: concepts like std::ranges::range are deliberately defined to work on both T& and T&&, so the abbreviated syntax is safe:

cpp
void process(std::ranges::range auto&& rg) {
    for (auto&& elem : rg) { use(elem); }
}

This works because std::ranges::range<T&> and std::ranges::range<T&&> are both valid. When designing your own concepts intended for forwarding references, model this: test the concept on the reference type directly, not the stripped type.

std::forward_like (C++23)

Header: <utility> — Paper: P2445R1

std::forward_like<T>(x) forwards x with the value category and const-ness of T, regardless of any relationship between T and x. This is the missing tool for two scenarios where plain std::forward falls short.

Problem 1 — forwarding a member through a wrapper

Forwarding a struct member through a forwarding reference to the wrapper used to require an if constexpr branch:

cpp
struct Wrapper { int data; };

// Before C++23 — verbose
void extract(auto&& w) {
    if constexpr (std::is_rvalue_reference_v<decltype(w)>)
        sink(std::move(w.data));   // rvalue wrapper → move the member
    else
        sink(w.data);              // lvalue wrapper → copy/ref the member
}

// C++23 — one line
void extract(auto&& w) {
    sink(std::forward_like<decltype(w)>(w.data));
}

Problem 2 — deducing-this member functions

C++23 "deducing this" (this auto&& self) lets a single member function handle all four combinations of const/non-const × lvalue/rvalue. std::forward_like is the intended tool here because self is an explicit parameter — you cannot use std::forward<decltype(*this)>:

cpp
struct MyType {
    int data;

    // Single getter replaces four const/ref-qualified overloads
    auto&& get(this auto&& self) {
        return std::forward_like<decltype(self)>(self.data);
    }
};

MyType obj{42};
obj.get();                    // returns int&
std::as_const(obj).get();    // returns const int&
std::move(obj).get();        // returns int&&
std::move(std::as_const(obj)).get();  // returns const int&&

How std::forward_like determines the return type

T (owner)const of U memberResult category
lvalue ref Owner&non-const MemberMember&
lvalue ref const Owner&non-const Memberconst Member&
rvalue ref Owner&&non-const MemberMember&&
rvalue ref const Owner&&non-const Memberconst Member&&

Rule: adopt the value category of T, and merge the const-qualifiers (const from either T or U is preserved). This is the "merge model" described in P2445R1.

Signature

cpp
// <utility>, since C++23
template<class T, class U>
constexpr auto&& forward_like(U&& x) noexcept;

T is the owner whose value category you want to borrow. U&& is the member (or any object) you want to forward. Unlike std::forward, T and U need not be related types.

Perfect Forwarding in Constructors

Forwarding constructor arguments to member sub-objects is the most common real-world use of perfect forwarding. Without it, every constructor that delegates to a member must either copy-construct (wasting a move) or provide a combinatorial explosion of lvalue/rvalue overloads.

The canonical idiom is to declare the constructor as a variadic template and forward each argument directly into the member's initialiser:

cpp
// C++11
template<typename T>
struct Wrapper {
    T value;

    template<typename... Args>
    explicit Wrapper(Args&&... args)
        : value(std::forward<Args>(args)...)   // forward directly into T's constructor
    {}
};

Wrapper<std::string> w1("hello");               // constructs in-place — no extra copy
std::string s = "world";
Wrapper<std::string> w2(s);                     // lvalue: copies
Wrapper<std::string> w3(std::move(s));          // rvalue: moves

When there is only one member argument, the single-parameter form is cleaner but identical in principle. Prefer explicit on such constructors to avoid accidental implicit conversions — the forwarding reference already accepts anything, so the compiler cannot warn without explicit.

cpp
// C++11 — single-member, single-arg case
template<typename T>
struct Box {
    T item;

    template<typename U>
    explicit Box(U&& u) : item(std::forward<U>(u)) {}
};

Note that this pattern deliberately does NOT forward *this (which is always an lvalue inside the constructor body). The forwarding applies only to the constructor's own parameters as they flow into member initialisers.

Abbreviated Function Templates and decltype(auto) Return (C++20)

C++20 abbreviated function templates let you write forwarding code without spelling out template<typename T> explicitly. A parameter declared auto&& is exactly a forwarding reference — the compiler generates a hidden template parameter for each auto it encounters.

cpp
// C++20
void sink(auto&& value) {
    // 'value' is a forwarding reference; decltype(value) gives the exact type
    process(std::forward<decltype(value)>(value));
}

Because there is no named template parameter T, you must use decltype to recover the type for std::forward. This is the same idiom as generic lambdas (which have used auto&& since C++14), now available on free functions and member functions as well.

decltype(auto) as the return type is the forwarding counterpart for return values: it deduces T&, T&&, or T exactly as the expression's value category dictates, preserving references where a plain auto would strip them.

cpp
// C++20 — forwarding return type preserves references
decltype(auto) identity(auto&& x) {
    return std::forward<decltype(x)>(x);   // returns T&, T&&, or T as appropriate
}

int  a = 42;
int& r = identity(a);       // deduces int& — no copy
int  v = identity(42);      // deduces int  — value

Combining auto&& parameters with decltype(auto) return gives you a zero-overhead transparent forwarder in three lines. The same technique scales to variadic packs: auto&&... args combined with decltype(args) in the expansion.

The Forwarding Wrapper Pattern

Transparent wrappers — logging, timing, retry, memoisation — are the highest-value application of perfect forwarding in production code. The goal is that the wrapper is invisible to the wrapped function: it receives every argument with its original value category intact, and it propagates the return value without stripping references or forcing extra copies.

The canonical building block combines std::invoke with std::forward so that the wrapper works with free functions, member function pointers, and function objects uniformly:

cpp
// C++17 — generic timing wrapper
#include <chrono>
#include <functional>
#include <utility>

template<typename F, typename... Args>
decltype(auto) timed(F&& f, Args&&... args) {
    auto t0 = std::chrono::steady_clock::now();
    decltype(auto) result = std::invoke(std::forward<F>(f),
                                        std::forward<Args>(args)...);
    auto elapsed = std::chrono::steady_clock::now() - t0;
    log_duration(elapsed);
    return result;   // decltype(auto) preserves reference returns
}

std::invoke normalises the call syntax: f(args...), obj.method(args...), and ptr_to_member(obj, args...) all work identically. Combined with std::forward, the callable itself is also forwarded — so a function object that is move-only is consumed correctly when passed as an rvalue.

For retry logic the same skeleton applies, but the callable must be called multiple times. In that case do not forward the callable on the first call — save std::forward<F>(f) for the final invocation, or accept only lvalue callables:

cpp
// C++17 — retry wrapper (callable called multiple times — do not forward on first N uses)
template<typename F, typename... Args>
decltype(auto) retry(int attempts, F&& f, Args&&... args) {
    while (--attempts > 0) {
        try {
            return std::invoke(f, args...);   // intentionally NOT forwarding yet
        } catch (...) { /* log and continue */ }
    }
    return std::invoke(std::forward<F>(f),    // last attempt — safe to forward
                       std::forward<Args>(args)...);
}

Common Mistakes

The two most damaging mistakes with perfect forwarding are double-moving and forgetting std::forward on the forwarded argument.

Double-move: forwarding an lvalue reference

When a parameter is declared as a plain lvalue reference (T&) and you apply std::forward or std::move to it, you silently move out of a caller-owned object:

cpp
// C++11 — BUG: lvalue reference treated as rvalue
void bad_sink(std::string& s) {
    store(std::move(s));   // caller's string is now in a moved-from state
    use(s);                // undefined behaviour — s may be empty or garbage
}

The fix is to accept a forwarding reference (T&&) only when you genuinely want to support moves, and to document clearly whether ownership is transferred.

Forwarding twice from the same parameter

std::forward may cast to rvalue, enabling a move. Applying it twice to the same parameter is undefined behaviour if the first call moved from the object:

cpp
// C++11 — BUG: forwarding the same argument twice
template<typename T>
void double_forward(T&& x) {
    first(std::forward<T>(x));    // may move x
    second(std::forward<T>(x));   // UB if x was moved from
}

Always forward a given argument exactly once, on its last use. If you must pass the same value to two callees, pass by value to the first and forward to the second, or pass as an lvalue to the first and forward to the second:

cpp
// C++11 — correct: lvalue access first, forward on last use
template<typename T>
void safe_two(T&& x) {
    first(x);                    // lvalue access — no move
    second(std::forward<T>(x));  // forward (potentially move) on last use
}

Forgetting std::forward entirely

A forwarding reference without std::forward always behaves as an lvalue — the rvalue optimisation is silently lost:

cpp
// C++11 — BUG: heavy object always copied, never moved
template<typename T>
void forgot_forward(T&& x) {
    sink(x);                   // x is an lvalue expression here — copies every time
    sink(std::forward<T>(x));  // correct — moves when caller passed an rvalue
}

Compilers do not warn about this by default because passing an lvalue is always legal. A code review checklist item: every T&& forwarding parameter should appear in exactly one std::forward<T>(...) call on its last use.

See Also

  • Move Semantics — value categories, rvalue references, and the mechanics that make forwarding useful
  • Value Categories — lvalue, xvalue, prvalue — the taxonomy that std::forward preserves
  • Variadic Templates — parameter packs let perfect forwarding scale to any argument count
  • Perfect Forwarding (idiom) — practical patterns for wrappers, middleware, and decorators