Reference Collapsing in C++ Template Deduction
Reference collapsing is the mechanism in C++ that resolves compound
reference types formed during template instantiation, type aliasing, and
deduction. When a function template accepts a forwarding reference
(written as T&&), template argument deduction
determines the concrete type of T based on whether an
lvalue or an rvalue is passed. Reference collapsing rules then dictate
how the deduced type combines with the parameter declaration to produce
the final reference type of the function argument. This synergy forms
the foundation of perfect forwarding in modern C++.
Forwarding References and Deduction Rules
A forwarding reference occurs exclusively when a function template
parameter is declared as T&&, where T
is a deduced template type parameter for that specific function.
During template argument deduction:
- If the passed argument is an lvalue of type
X,Tis deduced as an lvalue reference:X&. - If the passed argument is an rvalue of type
X,Tis deduced as the non-reference object type:X.
Because the parameter is declared as T&&,
substituting an lvalue deduction (X&) into
T&& results in a hypothetical
reference-to-reference type: X& &&. Since
direct references to references are illegal in C++ syntax, the language
applies reference collapsing rules to resolve this combination into a
valid type.
The Reference Collapsing Rules
C++ defines four possible combinations when references combine during template instantiation:
&+&collapses to&(lvalue reference to lvalue reference)&+&&collapses to&(rvalue reference to lvalue reference)&&+&collapses to&(lvalue reference to rvalue reference)&&+&&collapses to&&(rvalue reference to rvalue reference)
In short, if an lvalue reference is involved anywhere in the
combination, the resulting type collapses to an lvalue reference
(&). The only condition that yields an rvalue reference
(&&) is when both references are rvalue
references.
How Deduction and Collapsing Work Together
Consider the following function template:
template <typename T>
void process(T&& arg);Case 1: Passing an Lvalue
int x = 10;
process(x);xis an lvalue of typeint.- Template argument deduction assigns
T = int&. - Substituting
TintoT&&yieldsint& &&. - Reference collapsing transforms
int& &&intoint&. - The function parameter
argbecomes an lvalue reference:int&.
Case 2: Passing an Rvalue
process(20);20is an rvalue of typeint.- Template argument deduction assigns
T = int. - Substituting
TintoT&&yieldsint&&. - No collapsing is necessary because no reference-to-reference is formed.
- The function parameter
argbecomes an rvalue reference:int&&.
Role in Perfect Forwarding
This interplay between deduction and collapsing ensures that the
exact value category (lvalue or rvalue) of the caller's argument is
encoded directly into the template parameter T.
Utilities like std::forward<T> rely on this
encoded type:
- If
Tis deduced asint&,std::forward<T>casts the argument back to an lvalue (int&). - If
Tis deduced asint,std::forward<T>casts the argument back to an rvalue (int&&).
Without reference collapsing, the type system would reject template
instantiations involving lvalue references passed to
T&&, making generic wrapper functions and perfect
forwarding impossible in C++.