Perfect Forwarding
Preserve value categories through template wrappers using forwarding references, std::forward, and reference collapsing.
Perfect Forwardingsince C++11A 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:
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:
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):
for (auto&& elem : container) { /* elem is a forwarding ref */ } // C++11
auto fn = [](auto&& x) { /* x is a forwarding ref */ }; // C++14Reference Collapsing (C++11)
When T is deduced as a reference type, double-references are collapsed:
T deduced as | Parameter 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
// 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
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-placeThis 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:
// 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:
#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)); // okThe 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:
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:
// 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:
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 equivalentFor 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:
// 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:
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:
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:
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:
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