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, T is deduced as an lvalue reference: X&.
  • If the passed argument is an rvalue of type X, T is 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:

  1. & + & collapses to & (lvalue reference to lvalue reference)
  2. & + && collapses to & (rvalue reference to lvalue reference)
  3. && + & collapses to & (lvalue reference to rvalue reference)
  4. && + && 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);
  1. x is an lvalue of type int.
  2. Template argument deduction assigns T = int&.
  3. Substituting T into T&& yields int& &&.
  4. Reference collapsing transforms int& && into int&.
  5. The function parameter arg becomes an lvalue reference: int&.

Case 2: Passing an Rvalue

process(20);
  1. 20 is an rvalue of type int.
  2. Template argument deduction assigns T = int.
  3. Substituting T into T&& yields int&&.
  4. No collapsing is necessary because no reference-to-reference is formed.
  5. The function parameter arg becomes 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 T is deduced as int&, std::forward<T> casts the argument back to an lvalue (int&).
  • If T is deduced as int, 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++.