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++11A 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:
template<typename T>
void wrapper(T&& arg); // forwarding reference — T is deduced
void f(int&& x); // plain rvalue reference — not a forwarding referenceReference collapsing — the rule that resolves double-references produced by template substitution:
| Declared as | Deduced T | Result |
|---|---|---|
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.
template<typename T>
void forward_to_sink(T&& arg) {
sink(std::forward<T>(arg)); // preserves lvalue/rvalue-ness of arg
}Syntax
#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.
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
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
auto logged = [](auto&& fn, auto&&... args) {
log("calling");
return fn(std::forward<decltype(args)>(args)...);
};std::invoke-compatible wrapper
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:
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:
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:
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:
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:
// 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:
// 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:
template<std::integral I>
void foo(I&& i); // WRONG intent — I deduces to int& for lvaluesPassing 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:
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 wantedThe fix: strip the reference in the constraint
Use std::remove_cvref_t to make the concept test the underlying type:
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:
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:
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:
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)>:
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 member | Result category |
|---|---|---|
lvalue ref Owner& | non-const Member | Member& |
lvalue ref const Owner& | non-const Member | const Member& |
rvalue ref Owner&& | non-const Member | Member&& |
rvalue ref const Owner&& | non-const Member | const 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
// <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:
// 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: movesWhen 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.
// 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.
// 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.
// 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 — valueCombining 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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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::forwardpreserves - Variadic Templates — parameter packs let perfect forwarding scale to any argument count
- Perfect Forwarding (idiom) — practical patterns for wrappers, middleware, and decorators