본문으로 건너뛰기 C++ Reference Collapsing | Rules for T& and T&&

C++ Reference Collapsing | Rules for T& and T&&

C++ Reference Collapsing | Rules for T& and T&&

이 글의 핵심

Reference collapsing: how T&, T&& combinations collapse to a single reference, enabling forwarding references, std::forward, and template deduction.

What is reference collapsing?

You cannot write int& & directly, but nested references can appear through aliases, templates, and decltype. The compiler collapses them:

T&  &   -> T&
T&  &&  -> T&
T&& &   -> T&
T&& &&  -> T&&

Rule of thumb: if any & is involved, the result is &; only && + && stays &&.

With forwarding references

Here is the func implementation:

template<typename T>
void func(T&& arg) {}
int x = 10;
func(x);       // T = int&,   T&& -> int& && -> int&
func(10);      // T = int,    T&& -> int&&

auto&&

int x = 10;
auto&& a = x;           // int& (collapsing)
auto&& b = 10;          // int&&

std::forward sketch

template<typename T>
T&& forward(typename std::remove_reference_t<T>& arg) noexcept {
    return static_cast<T&&>(arg);
}

Pitfalls

  • std::move in a forwarding wrapper forces rvalue—use std::forward<T> when preserving category.
  • decltype((x)) can introduce references—pair with remove_reference_t when needed.

Deep dive: Why collapsing exists

Problem without collapsing

Before C++11, you couldn’t have references to references:

typedef int& IntRef;
typedef IntRef& IntRefRef;  // ❌ Error in C++03

Solution: Collapsing rules

C++11 introduced reference collapsing to make templates work with references:

template<typename T>
void func(T&& arg);  // T can be int, int&, or int&&
int x = 10;
func(x);  // T = int&, arg type = int& && -> int&

Without collapsing, T = int& would make T&& illegal (int& &&).

Complete collapsing table with examples

TT&T&&Use case
intint&int&&Normal types
int&int&int&Lvalue passed to template
int&&int&int&&Rvalue passed to template
const int&const int&const int&Const lvalue

Practical example

template<typename T>
void wrapper(T&& arg) {
    std::cout << std::boolalpha;
    std::cout << "Is lvalue ref: " << std::is_lvalue_reference_v<decltype(arg)> << "\n";
    std::cout << "Is rvalue ref: " << std::is_rvalue_reference_v<decltype(arg)> << "\n";
}
int x = 10;
wrapper(x);        // T=int&,  arg=int&   -> lvalue ref: true
wrapper(10);       // T=int,   arg=int&&  -> rvalue ref: true
wrapper(std::move(x));  // T=int, arg=int&& -> rvalue ref: true

Interaction with std::forward

How forward uses collapsing

template<typename T>
T&& forward(std::remove_reference_t<T>& arg) noexcept {
    return static_cast<T&&>(arg);
}
// When T = int& (lvalue case):
// int& && forward(int& arg) -> int& forward(int& arg)
// Returns: static_cast<int& &&>(arg) -> static_cast<int&>(arg)
// When T = int (rvalue case):
// int&& forward(int& arg)
// Returns: static_cast<int&&>(arg)

Real-world forwarding example

template<typename T>
class Optional {
    alignas(T) unsigned char storage_[sizeof(T)];
    bool hasValue_ = false;
    
public:
    template<typename U>
    void emplace(U&& value) {
        if (hasValue_) {
            reinterpret_cast<T*>(storage_)->~T();
        }
        // Collapsing ensures correct construction:
        // U=int&  -> new(storage_) T(static_cast<int&>(value))
        // U=int   -> new(storage_) T(static_cast<int&&>(value))
        new(storage_) T(std::forward<U>(value));
        hasValue_ = true;
    }
};

decltype and collapsing

decltype((x)) adds reference

int x = 10;
decltype(x) a = x;      // int
decltype((x)) b = x;    // int& (extra parens make it lvalue expression)
template<typename T>
void func(T&& arg) {
    decltype(arg) local = arg;  // Preserves reference type
}

Pitfall: Double reference

template<typename T>
void process(T&& arg) {
    using RefType = decltype(arg);  // Could be int& or int&&
    RefType&& x = std::forward<T>(arg);  // ❌ Potential double reference
}

Fix: Use std::remove_reference_t:

template<typename T>
void process(T&& arg) {
    using ValueType = std::remove_reference_t<decltype(arg)>;
    ValueType copy = arg;  // Always makes a copy
}

Common mistakes

Mistake 1: Assuming T&& is always rvalue reference

template<typename T>
void bad(T&& arg) {
    // ❌ Wrong assumption: arg might be lvalue reference
    std::vector<T> vec;
    vec.push_back(std::move(arg));  // Moves even from lvalue!
}
template<typename T>
void good(T&& arg) {
    std::vector<std::remove_reference_t<T>> vec;
    vec.push_back(std::forward<T>(arg));  // ✅ Preserves category
}

Mistake 2: Forgetting collapsing in type aliases

template<typename T>
struct Wrapper {
    using RefType = T&&;  // Not always rvalue reference!
};
Wrapper<int&>::RefType x;  // int& (not int& &&)

Mistake 3: auto&& in wrong context

std::vector<int> getVec();
auto&& vec = getVec();  // ✅ OK: extends lifetime of temporary
auto&& first = vec[0];  // ✅ OK: binds to lvalue element
// ❌ Dangerous:
auto&& dangling = getVec()[0];  // Temporary vector destroyed!

Performance implications

Zero overhead: Reference collapsing is compile-time only. No runtime cost. Code generation comparison (GCC 13, -O3):

// Hand-written overloads
void process(int& x) { /* ... */ }
void process(int&& x) { /* ... */ }
// Forwarding reference
template<typename T>
void process(T&& x) { /* ... */ }

Both generate identical assembly after template instantiation.

Advanced: Collapsing with variadic templates

Here is the multiForward implementation:

template<typename... Args>
void multiForward(Args&&... args) {
    // Each arg follows collapsing rules independently
    callee(std::forward<Args>(args)...);
}
int x = 10;
multiForward(x, 20, std::string("hello"));
// Args = int&, int, std::string
// args = int&, int&&, std::string&&

Debugging variadic collapsing

template<typename T>
void printType() {
    std::cout << __PRETTY_FUNCTION__ << "\n";
}
template<typename... Args>
void debug(Args&&... args) {
    (printType<Args>(), ...);  // C++17 fold expression
}
int x = 10;
debug(x, 20);
// Prints:
// void printType() [T = int&]
// void printType() [T = int]

Compiler diagnostics

GCC/Clang: View collapsed types

# Compile with -fdump-tree-original to see template instantiations
g++ -std=c++20 -fdump-tree-original test.cpp

MSVC: Template instantiation details

# Use /d1reportAllClassLayout to see all template expansions
cl /std:c++20 /d1reportAllClassLayout test.cpp

Keywords

C++, reference collapsing, forwarding reference, templates, C++11, type deduction, perfect forwarding, std::forward


Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. Reference collapsing: how T&, T&& combinations collapse to a single reference, enabling forwarding references, std::forw…

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers C++, reference-collapsing, templates, type-deduction.