Perfect Forwarding & Universal References
A T&& that isn't an rvalue reference at all — and the one-line trick that keeps a wrapper function from copying anything it doesn't have to.
Advanced
T&& means two different things depending on context, and mixing
them up is the single most common source of confusion here. On a concrete, non-deduced
type, T&& is an ordinary rvalue reference — it only binds to
rvalues. In a context where T is itself being deduced (a function template
parameter, or auto&&), T&& is a
forwarding reference (Scott Meyers' term "universal reference" is the more
widely recognized name for the same thing) — it binds to anything, lvalue or
rvalue, and T is deduced differently depending on which.
void f(Widget &&w); // NOT a universal reference — Widget is concrete, not deduced.
// Only binds to rvalues, exactly like any other rvalue reference.
template <typename T>
void g(T &&arg); // universal reference — T is deduced right here, at the call site.
Widget w;
g(w); // arg binds to an lvalue — T deduced as Widget&, so T&& collapses to Widget&
g(Widget()); // arg binds to an rvalue — T deduced as Widget, so T&& stays Widget&&
&& syntax by itself
— the same two characters mean something entirely different depending on whether a type
is being deduced right there or was already fixed beforehand.
auto deduction can still produce one internally — when that happens,
the compiler collapses the two references into one according to a fixed rule: an
lvalue reference wins. Only "rvalue reference to rvalue reference" collapses
to an rvalue reference; every other combination collapses to an lvalue reference.
| Written internally | Collapses to |
|---|---|
T& & | T& |
T& && | T& |
T&& & | T& |
T&& && | T&& |
g(w) example above: T
is deduced as Widget&, the parameter's declared type
T&& becomes Widget& &&, and that collapses to
Widget& — an ordinary lvalue reference, which is why arg
can bind to the lvalue w at all.
std::move is unconditional: it always casts its argument to an rvalue
reference, regardless of what was passed in. std::forward<T> is
conditional: it casts to an rvalue reference only if T was deduced from an
rvalue in the first place — otherwise it passes the argument through as the lvalue it
already was. That's the entire mechanism "perfect forwarding" refers to: preserving whatever
value category the caller originally used, through one extra layer of function call.
template <typename T>
void wrapper(T &&arg) {
// target(arg); // WRONG here — arg is itself a named variable, so it's always an lvalue
// inside wrapper's body, even when T&& deduced as an rvalue reference
target(std::forward<T>(arg)); // RIGHT — forwards arg as whatever it originally was at the call site
}
std::forward<T>(arg) is what recovers the original rvalue-ness when
T says there was one — plain arg never does, and neither does
std::move(arg), which would force an rvalue cast even for a caller who passed an
lvalue on purpose.
T by value or by const& either
copies unconditionally or can never move; with a universal reference plus
std::forward, the wrapper is exactly as cheap as calling the target function
directly, whether the caller passed an lvalue or a temporary.
Widget that prints which constructor runs, forwarded through a wrapper
twice — once with a named variable (lvalue), once with a temporary (rvalue) — so
you can see the copy constructor and move constructor fire on exactly the calls that should
trigger each one:
TopNotchNote/cpp/perfect_forwarding_wrapper_demo.cpp