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::movein a forwarding wrapper forces rvalue—usestd::forward<T>when preserving category.decltype((x))can introduce references—pair withremove_reference_twhen 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
| T | T& | T&& | Use case |
|---|---|---|---|
int | int& | 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
Related posts
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.
Related Articles (Internal Links)
Other articles related to this topic.
- C++ Universal Reference | ‘유니버설 레퍼런스’ 가이드
- C++ auto 키워드 | ‘타입 추론’ 가이드
- C++ auto 타입 추론 | 복잡한 타입을 컴파일러에 맡기기
Keywords Covered in This Article (Related Search Terms)
This article covers C++, reference-collapsing, templates, type-deduction.