How Does std::forward Work in C++?
Perfect forwarding in C++ enables a generic wrapper function to
accept arguments and pass them along to another target function while
preserving their original value category, whether an lvalue or an
rvalue, as well as their const qualifiers. Without perfect
forwarding, arguments passed by name within a function body always
evaluate as lvalues, causing unexpected deep copies instead of efficient
moves. C++ achieves this through a combination of universal references
(forwarding references), template argument deduction, reference
collapsing rules, and the conditional casting mechanism provided by
std::forward.
The Core Problem in Generic Code
In generic programming, wrapper functions often accept arguments to construct objects or delegate tasks. Consider a factory pattern or a logging wrapper:
template
T create(Arg arg) {
return T(arg);
}Passing parameters by value can incur expensive copies. If you
instead pass by const Arg&, temporary objects and
rvalues cannot be moved, and non-const lvalues cannot be modified.
Overloading for every possible permutation of const and
non-const references quickly leads to an exponential explosion of code
when a function accepts multiple arguments.
Furthermore, even if an rvalue reference like
Arg&& is accepted, the named parameter inside the
function body itself is an lvalue. If passed directly to another
function, it resolves to an lvalue overload, preventing move semantics
unless cast explicitly.
Forwarding References and Type Deduction
To solve this, C++11 introduced forwarding references (also called
universal references). A forwarding reference takes the form
T&&, where T is a deduced template
parameter for that specific function call:
template
void wrapper(T&& arg);When an argument is passed to wrapper(T&&):
- If the caller passes an lvalue of type
X,Tis deduced as an lvalue reference:X&. The parameter type becomesX& &&. - If the caller passes an rvalue of type
X,Tis deduced as the raw object type:X. The parameter type becomesX&&.
Reference Collapsing Rules
Because C++ does not allow references to references directly in user code, the compiler applies reference collapsing during template instantiation according to strict rules:
&with&collapses to&&with&&collapses to&&&with&collapses to&&&with&&collapses to&&
Applying these rules to the wrapper:
- Passing an lvalue (
X&):X& &&collapses toX&(an lvalue reference). - Passing an rvalue (
X):X&&remainsX&&(an rvalue reference).
This deduction ensures the function signature adapts dynamically to preserve the argument's reference type.
How std::forward Works
While the deduced type retains knowledge of whether the caller
supplied an lvalue or an rvalue, the parameter arg inside
the function body is an entity with a name. By definition in C++, any
expression consisting solely of a named variable has an lvalue value
category.
std::forward restores the original value category
through a conditional static cast. Its standard implementation resembles
the following:
template
constexpr T&& forward(std::remove_reference_t& param) noexcept {
return static_cast(param);
}
template
constexpr T&& forward(std::remove_reference_t&& param) noexcept {
static_assert(!std::is_lvalue_reference_v,
"Template argument must not be an lvalue reference for rvalue overload.");
return static_cast(param);
}Case 1: An Lvalue Was Passed to the Wrapper
- Caller passes an lvalue
wof typeWidget. Tis deduced asWidget&.- The wrapper receives
Widget& arg. - Inside the wrapper,
std::forward(arg)is called. - Inside
std::forward, the return type isWidget& &&, which collapses toWidget&. static_cast(arg)returns an lvalue reference, preserving lvalue semantics.
Case 2: An Rvalue Was Passed to the Wrapper
- Caller passes a temporary or moved value
Widget{}. Tis deduced asWidget.- The wrapper receives
Widget&& arg. - Inside the wrapper,
std::forward(arg)is called. - Inside
std::forward, the return type isWidget&&. static_cast(arg)casts the named lvalueargback into an rvalue expression (xvalue), allowing downstream functions to move it.
Practical Implementation Example
A standard application of perfect forwarding is in functions like
std::make_unique or container emplace operations:
#include
#include
#include
class Resource {
public:
Resource(const std::string& name) {
std::cout << "Copied name: " << name << '\n';
}
Resource(std::string&& name) {
std::cout << "Moved name: " << name << '\n';
}
};
template
std::unique_ptr make_resource(Args&&... args) {
return std::unique_ptr(new T(std::forward(args)...));
}
int main() {
std::string text = "Persistent";
// Calls the copy constructor because 'text' is an lvalue
auto r1 = make_resource(text);
// Calls the move constructor because a temporary is an rvalue
auto r2 = make_resource(std::string("Temporary"));
}By encoding the argument's category into the template parameter
Args and using std::forward(args)..., each
argument arrives at the Resource constructor in its
original state without unnecessary allocations or duplicate wrapper
overloads.