Perfect Forwarding with std::forward
When you write a wrapper function that passes arguments through to another function, a problem emerges: inside the wrapper, all parameters are lvalues (they have names), so any rvalue-ness from the caller is silently lost. Perfect forwarding is the technique — using forwarding references and std::forward — that preserves the original value category all the way through to the eventual function call. It is what makes std::make_unique, std::vector::emplace_back, and similar factory utilities work efficiently for both lvalues and rvalues.
The forwarding problem
A named parameter is always an lvalue inside a function body — even if the caller passed an rvalue. This means a naive wrapper always triggers the copy path, never the move path, even when the caller intended a move.
void process(std::string& s) { std::cout << "lvalue path\n"; }
void process(std::string&& s) { std::cout << "rvalue path\n"; }
// Naive wrapper — always lvalue, rvalue-ness is lost:
void wrapper(std::string s) {
process(s); // s has a name → lvalue → always calls lvalue overload
}
std::string name = "Alice";
wrapper(name); // prints: lvalue path (correct)
wrapper(std::move(name)); // ALSO prints: lvalue path (WRONG — move lost!)Forwarding references (T&&)
When a function template parameter is written as T&& where T is a deduced template parameter, it is not an rvalue reference — it is a forwarding reference (also called a universal reference). It can bind to both lvalues and rvalues, and reference collapsing rules ensure that T is deduced as Widget& for lvalues and Widget for rvalues.
// T&& in a template = forwarding reference (NOT an rvalue reference):
template<typename T>
void wrapper(T&& arg) {
// T is deduced as:
// Widget& when called with an lvalue → T&& collapses to Widget&
// Widget when called with an rvalue → T&& stays Widget&&
process(arg); // still wrong: arg has a name → always lvalue
}
// Reference collapsing rules:
// T = Widget& → T&& = Widget& && = Widget& (lvalue ref)
// T = Widget → T&& = Widget&& (rvalue ref)std::forward — conditional cast
std::forward<T>(arg) is a conditional cast: if T was deduced as an lvalue reference type (Widget&), it returns arg as an lvalue; if T was deduced as a non-reference type (Widget), it returns arg as an rvalue (xvalue). This restores the value category the caller originally had.
template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg)); // preserves value category!
}
std::string name = "Alice";
wrapper(name); // T = string& → forward returns lvalue → lvalue path
wrapper(std::move(name)); // T = string → forward returns rvalue → rvalue path
wrapper("Bob"); // T = string → forward returns rvalue → rvalue pathAlways write std::forward<T>(arg) with the explicit template argument — the compiler cannot deduce it. And always forward a parameter at most once: forwarding moves from it (if rvalue), so it should not be used again after.
Practical use: factory functions
Perfect forwarding enables factory utilities like std::make_unique and emplace_back to construct objects in place using whatever arguments the caller provides, preserving their value categories for maximum efficiency.
// Simplified make_unique:
template<typename T, typename... Args>
std::unique_ptr<T> my_make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
struct Widget {
Widget(std::string name, int value);
};
std::string n = "foo";
auto w = my_make_unique<Widget>(n, 42);
// n is lvalue → forwarded as lvalue → string copy constructor called for 'name'
auto w2 = my_make_unique<Widget>(std::move(n), 42);
// n is xvalue → forwarded as rvalue → string move constructor called for 'name'Key rules to remember
T&& in a template parameter is a forwarding reference, not an rvalue reference
It binds to both lvalues and rvalues. The distinction from a plain rvalue reference (Widget&&) is that T is deduced.
std::forward<T> restores the value category — always pass the template parameter explicitly
std::forward<T>(arg) is a conditional cast: lvalue if T is an lvalue ref type, rvalue otherwise. Without the <T>, it cannot work correctly.
Only forward a parameter once
After std::forward<T>(arg), arg may have been moved from. Using it again is undefined behavior for rvalues.
Use std::move for unconditional 'I'm done with this'; use std::forward for 'pass this along as-is'
move is for transferring out of your scope. forward is for relay — passing through to another function while preserving the original caller's intent.