C++ / TEMPLATES AND GENERIC PROGRAMMING
Perfect forwarding and reference collapsing
Deduce T for any argument, collapse T&& by hand, and use std::forward<T> to relay a caller's value category and constness without copying or over-moving.
What you will learn
- Deduce T yourself: lvalue gives T = X&, rvalue gives T = X, then collapse T&&
- Restore the caller's value category with std::forward<T>, never with std::move
- Tell a real forwarding reference from a plain T&& in a class template member
- Forward a parameter once, at its last use, since forwarding permits a move
Understanding Perfect forwarding and reference collapsing
In a function template, T&& is only an rvalue reference once T is known. During deduction it plays a second role: an lvalue argument of type X deduces T as X& (or const X&), while an rvalue argument deduces plain X. Substituting the first case gives X& &&, and since C++ has no reference to reference, the pair collapses by one rule worth memorising: & & becomes &, & && becomes &, && & becomes &, and only && && stays &&. Lvalue-ness always wins the collapse, which is precisely why one T&& parameter binds to temporaries, named objects, and const objects alike.
The category the caller used is now recorded in T, not in the parameter. Inside the body, arg names an object, and naming an object always produces an lvalue expression, so handing arg onwards selects copy-taking overloads even when the caller passed a temporary. std::forward<T>(arg) is nothing more than static_cast<T&&>(arg): with T = X& the cast collapses back to X& and leaves an lvalue alone, and with T = X it yields X&& and therefore an xvalue. That makes forward a conditional move whose condition was fixed at deduction time, which is exactly the information std::move discards.
Collapsing only helps where a reference is genuinely deduced in that parameter's own template. const T&&, std::vector<T>&&, and a T&& parameter of a member function of an already-instantiated class template are ordinary rvalue references that reject lvalues, whereas auto&& follows the T&& rule and does collapse. Because a forwarding parameter matches every argument exactly, it also outranks an overload like f(const X&) for non-const lvalues, so constrain it when it shares a name with other functions. And since forwarding grants permission to move, forward each parameter once, at its last use.
<iostream>
<string>
<type_traits>
<utility>
void consume(const std::string&) { std::cout << "consume(const std::string&)\n"; }
void consume(std::string&&) { std::cout << "consume(std::string&&)\n"; }
template <typename T>
void relay(T&& arg) {
const bool lref = std::is_lvalue_reference<T>::value;
std::cout << " T = " << (lref ? "std::string&" : "std::string")
<< ", T&& = " << (lref ? "std::string& (& && collapsed to &)" : "std::string&&")
<< '\n';
std::cout << " plain arg -> ";
consume(arg); // arg names an object, so it is an lvalue whatever T is
std::cout << " forward<T> -> ";
consume(std::forward<T>(arg)); // static_cast<T&&>(arg) puts the category back
}
int main() {
std::string s = "ada";
std::cout << "relay(s):\n";
relay(s);
std::cout << "relay(std::string{\"ada\"}):\n";
relay(std::string{"ada"});
}
A deduced T&& parameter stores the caller's value category inside T through reference collapsing, and std::forward<T> is the cast that reads it back out.
Worked examples
std::move in a relay steals from the caller
Shows that std::move casts unconditionally while std::forward respects the deduced T, using a type that marks its moved-from source.
<iostream>
<utility>
struct Payload {
int value;
Payload(int v) : value(v) {}
Payload(const Payload& o) : value(o.value) { std::cout << "copy\n"; }
Payload(Payload&& o) : value(o.value) { o.value = -1; std::cout << "move (source emptied)\n"; }
};
void store(Payload p) { std::cout << "stored " << p.value << '\n'; }
template <typename T> void goodRelay(T&& p) { store(std::forward<T>(p)); }
template <typename T> void badRelay(T&& p) { store(std::move(p)); }
int main() {
Payload a{7};
std::cout << "goodRelay(a) with std::forward\n";
goodRelay(a);
std::cout << "a.value = " << a.value << "\n\n";
Payload b{7};
std::cout << "badRelay(b) with std::move\n";
badRelay(b);
std::cout << "b.value = " << b.value << '\n';
}
Example explained
Line 1goodRelay deduces T = Payload&, so static_cast<T&&> collapses to Payload& and store's by-value parameter is copy constructed.
Line 2badRelay's std::move(p) is static_cast<Payload&&> no matter what T is, so the move constructor runs and writes -1 into the caller's object.
Line 3Both call sites look identical at the call, yet a keeps 7 while b is left at -1 - the bug is silent because std::move always compiles here.
Line 4store takes Payload by value, which is why the copy or move you see is that parameter being constructed.
Not every && is a forwarding reference
Contrasts a T&& member parameter of an instantiated class template with a member template's deduced U&&, and shows auto&& collapsing the same way.
<iostream>
<type_traits>
<utility>
template <typename T>
struct Box {
// T is already fixed when Box<int> is instantiated: plain rvalue reference, no deduction.
void put(T&& v) { std::cout << "put(T&&) took the rvalue " << v << '\n'; }
// U is deduced per call, so U&& really is a forwarding reference.
template <typename U>
void putAny(U&& v) {
std::cout << "putAny got an "
<< (std::is_lvalue_reference<U>::value ? "lvalue " : "rvalue ")
<< v << '\n';
}
};
int main() {
Box<int> box;
int n = 3;
// box.put(n); // error: cannot bind an int lvalue to int&&
box.put(7);
box.putAny(n);
box.putAny(7);
auto&& r1 = n; // int& after & && collapses
auto&& r2 = 7; // int&&
std::cout << std::is_lvalue_reference<decltype(r1)>::value
<< std::is_rvalue_reference<decltype(r2)>::value << '\n';
}
Example explained
Line 1Box<int>::put is literally void put(int&&): nothing is deduced in that parameter, so no collapsing happens and the commented-out lvalue call is rejected.
Line 2putAny deduces U = int& for n, collapsing int& && to int&, and U = int for the literal 7.
Line 3auto&& obeys the same deduction rule as T&&, so decltype(r1) is int& and decltype(r2) is int&&, printing 1 and 1.
const rides along inside T
Demonstrates that deduction records constness in T as well as the value category, and what a missing forward costs.
<iostream>
<utility>
void inspect(int&) { std::cout << "int&\n"; }
void inspect(const int&) { std::cout << "const int&\n"; }
void inspect(int&&) { std::cout << "int&&\n"; }
template <typename T> void relayFwd(T&& x) { inspect(std::forward<T>(x)); }
template <typename T> void relayPlain(T&& x) { inspect(x); }
int main() {
int a = 1;
const int c = 2;
std::cout << "relayFwd(a): "; relayFwd(a);
std::cout << "relayFwd(c): "; relayFwd(c);
std::cout << "relayFwd(3): "; relayFwd(3);
std::cout << "relayFwd(move(a)): "; relayFwd(std::move(a));
std::cout << "relayPlain(3): "; relayPlain(3);
}
Example explained
Line 1A const lvalue deduces T = const int&, so the const is stored in T and survives the collapse; std::forward neither adds nor removes const.
Line 2The literal 3 and std::move(a) are both rvalues, so T = int and static_cast<int&&> selects inspect(int&&).
Line 3relayPlain shows what is lost without the cast: x is a non-const named lvalue, so inspect(int&) wins even though the caller passed a temporary.
Important notes
std::forward moves nothing by itself; it only changes an expression's value category. The move, if any, happens in the callee when it selects a move constructor or move assignment.
Deduction strips the argument's top-level reference, so T for a forwarding parameter is deduced as X& or X but never as X&&; the && && case only appears when you apply && to a type alias that is already an rvalue reference.
Common mistakes
Writing std::move(arg) instead of std::forward<T>(arg) inside the relay: it compiles for every call, so a caller who passed a named object has it gutted after what looked like a read-only call.
Dropping the cast and writing sink(arg) because the parameter was declared with &&: arg is a named lvalue, so a temporary from the caller is copied instead of moved and the factory quietly copies everything.
Declaring const T&& or std::vector<T>&& and expecting lvalues to bind: neither is a forwarding reference, no collapsing occurs, and lvalue calls fail to compile.
Try it yourself
Change, predict, then run
Write template <class T> void trace(T&& x) that forwards to two describe overloads, one taking const std::string& and one taking std::string&&, then call it with a named string, a const string, a temporary, and std::move(named), writing down the four expected lines before you run it. Replace std::forward<T> with std::move and note exactly which lines change.
Open the C++ workspaceCheck your understanding
Why is template <class T> void relay(T&& x) { sink(std::move(x)); } wrong, even though it compiles for every argument you throw at it?
- std::move casts to an rvalue unconditionally, so an object the caller merely named can be moved from without asking
- std::move fails to compile whenever T has been deduced as an lvalue reference
- std::move makes a copy first, so each relayed call costs one extra copy
- std::move requires a move constructor, so relay rejects copy-only types
Show answer
std::move is static_cast<remove_reference_t<T>&&>, which throws away the & that deduction recorded in T; std::forward keeps it because static_cast<T&&> collapses back to an lvalue reference when T = X&. Option 1 is the tempting one and is exactly backwards: std::move compiles fine for an lvalue-reference T because it strips the reference first, which is why this bug shows up as a mysteriously emptied caller object rather than a compiler error.