Item 28:理解 reference collapsing (Understand Reference Collapsing)
Overview Table
| 主題 | 重點 |
|---|---|
| 問題背景 | C++ 禁止使用者宣告 reference to reference,但編譯器可在特定 context 產生它,並依規則塌縮成單一 reference |
| collapsing 規則 | 兩個 reference 中任一為 lvalue reference → 結果為 lvalue reference;兩者皆為 rvalue reference → 結果才是 rvalue reference |
| 四種發生 context | ① template instantiation ② auto 型別生成 ③ typedef / alias declaration 的建立與使用 ④ decltype |
| encoding 機制 | 傳 lvalue → T 推導為 T&(lvalue reference);傳 rvalue → T 推導為非 reference 型別(注意不對稱) |
| std::forward 原理 | 利用 T 中編碼的 lvalue/rvalue 資訊 + static_cast<T&&> + reference collapsing 完成條件式轉型 |
| universal reference 真相 | 並非新的 reference 種類,而是「型別推導區分 lvalue/rvalue + 發生 reference collapsing」時的 rvalue reference |
型別推導的 lvalue/rvalue 編碼機制
傳入 universal reference 參數的引數,其 lvalue/rvalue 屬性會被編碼進 T:
template<typename T>
void func(T&& param); // param 是 universal reference
Widget widgetFactory(); // 回傳 rvalue 的函式
Widget w; // 變數(lvalue)
func(w); // 傳 lvalue:T 推導為 Widget&(lvalue reference)
func(widgetFactory()); // 傳 rvalue:T 推導為 Widget(非 reference)
| 引數類別 | T 的推導結果 |
param 最終型別 |
|---|---|---|
lvalue(型別 Widget) |
Widget&(lvalue reference 編碼) |
Widget& |
rvalue(型別 Widget) |
Widget(非 reference 編碼) |
Widget&& |
編碼機制不對稱:lvalue 編碼為 lvalue reference,rvalue 卻編碼為「非 reference」(而不是 rvalue reference)。這個不對稱正是 std::forward 能運作的關鍵。
此編碼只發生在引數初始化 universal reference 參數時。若參數是一般的 T&、const T&、pass-by-value,或形式不是恰好 T&&(如 const T&&、std::vector<T>&&),T 就不會編碼引數的 lvalue/rvalue 屬性——那些 && 只是普通 rvalue reference(見 Item 24 與 Item 1 的一般推導規則)。
Reference Collapsing 規則與四種 Context
使用者不能宣告 reference to reference:
int x;
auto& & rx = x; // 錯誤!不能宣告 reference to reference
但編譯器在替換推導結果時可能產生它,此時套用 collapsing 規則:
| 組合 | 結果 |
|---|---|
& + & |
&(lvalue reference) |
& + && |
&(lvalue reference) |
&& + & |
&(lvalue reference) |
&& + && |
&&(rvalue reference) |
口訣:任一方是 lvalue reference(&)就塌縮成 lvalue reference;只有 && + && 才得到 rvalue reference。
func(w) 傳 lvalue 的塌縮流程:
T 推導為 Widget&
│ 代入 T&&
▼
void func(Widget& && param); ← reference to reference(編譯器產生)
│ reference collapsing:& + && → &
▼
void func(Widget& param); ← 最終簽名:lvalue reference
Reference collapsing 發生在四種 context(Things to Remember 要點):
| # | Context | 範例 |
|---|---|---|
| 1 | Template instantiation | func(w) 將 Widget& 代入 T&& |
| 2 | auto 型別生成 |
auto&& w1 = w; → Widget& && → Widget& |
| 3 | typedef / alias declaration |
Widget<int&> 中 typedef T&& R; → int& && → int& |
| 4 | decltype |
decltype 型別分析中出現 reference to reference 時塌縮 |
// context 3 範例:typedef 中的塌縮
template<typename T>
class Widget {
public:
typedef T&& RvalueRefToT; // 名字會騙人!
};
Widget<int&> w; // T = int&:int& && 塌縮為 int&
// RvalueRefToT 實際上是 lvalue reference(int&)
typedef / alias 的名稱不保證語意:上例 RvalueRefToT 在 Widget<int&> 實例化後其實是 lvalue reference。閱讀 template 程式碼時,凡涉及 reference 的 typedef 都要先做塌縮再下結論。
std::forward 的運作原理
std::forward<T> 的任務:當且僅當 T 編碼為 rvalue(即 T 為非 reference 型別)時,把參數(在函式內本身永遠是 lvalue)轉型成 rvalue。
// C++11 簡化實作(位於 namespace std)
template<typename T>
T&& forward(typename remove_reference<T>::type& param)
{
return static_cast<T&&>(param); // 靠 reference collapsing 決定結果
}
// C++14 可用 remove_reference_t 更精簡
template<typename T>
T&& forward(remove_reference_t<T>& param)
{
return static_cast<T&&>(param);
}
兩種實例化的推導過程:
| 傳入的引數 | T |
回傳型別 T&& 塌縮後 |
static_cast<T&&> 效果 |
|---|---|---|---|
lvalue Widget |
Widget& |
Widget& && → Widget& |
轉型無作用,回傳 lvalue(lvalue reference 是 lvalue) |
rvalue Widget |
Widget |
Widget&&(無塌縮) |
轉為 rvalue(函式回傳的 rvalue reference 是 rvalue) |
// T = Widget&(引數為 lvalue)時,塌縮後的最終版本:
Widget& forward(Widget& param)
{ return static_cast<Widget&>(param); } // cast 無作用 → 回傳 lvalue
// T = Widget(引數為 rvalue)時,無塌縮的最終版本:
Widget&& forward(Widget& param)
{ return static_cast<Widget&&>(param); } // lvalue 參數被 cast 成 rvalue
上述實作為教學用簡化版,並非完全符合 Standard(省略了部分介面細節,例如處理 rvalue 引數的 overload 與 constexpr/noexcept 修飾),但行為模型正確。
這解釋了為什麼呼叫時必須寫 std::forward<T>(param)——顯式提供 T 才能把「引數原本是 lvalue 還是 rvalue」的編碼資訊帶進來。
Universal Reference 的真相
Universal reference 不是新的 reference 種類,它其實就是 rvalue reference,只是出現在同時滿足兩個條件的 context:
- 型別推導會區分 lvalue 與 rvalue:型別
T的 lvalue 推導為T&,rvalue 推導為T。 - 會發生 reference collapsing。
auto&& w1 = w; // lvalue:auto = Widget&
// Widget& && → 塌縮 → w1 是 Widget&(lvalue reference)
auto&& w2 = widgetFactory(); // rvalue:auto = Widget
// 無塌縮 → w2 是 Widget&&(rvalue reference)
「universal reference」是一層有用的抽象:讓你不必每次都手動走「辨認 collapsing context → 分別推導 lvalue/rvalue 的型別 → 代入後套用塌縮規則」這套流程。
Exam/Test Patterns
| 情境關鍵字 | 答案 |
|---|---|
「& + && 塌縮成什麼?」 |
lvalue reference(任一方為 & 即為 &;只有 && + && 得 &&) |
| 「reference collapsing 發生在哪四種 context?」 | template instantiation、auto 型別生成、typedef/alias declaration、decltype |
「傳 lvalue 給 T&&,T 推導為?」 |
Widget&(lvalue reference);傳 rvalue 則 T = Widget(非 reference,注意不對稱) |
「auto&& w1 = w; 中 w1 的型別?」 |
Widget&(lvalue 使 auto = Widget&,塌縮後為 lvalue reference) |
| 「使用者可以宣告 reference to reference 嗎?」 | 不行,編譯錯誤;只有編譯器能在 collapsing context 中產生並塌縮它 |
「std::forward 怎麼知道要不要轉成 rvalue?」 |
看 T:T 為非 reference(編碼 rvalue)→ static_cast<T&&> 產生 rvalue reference;T 為 lvalue reference → 塌縮後轉型無作用 |
| 「universal reference 是什麼?」 | 「型別推導區分 lvalue/rvalue + 發生 reference collapsing」的 context 中的 rvalue reference,不是新的 reference 種類 |
「Widget<int&> 中 typedef T&& R 的 R 是?」 |
int&(int& && 塌縮)——名為 RvalueRef 實為 lvalue reference |
Related Notes
- 06-Move-Semantics-And-Perfect-Forwarding/01-Std-Move-And-Std-Forward(Item 23:
std::forward的使用,本 Item 解釋其實作原理) - 06-Move-Semantics-And-Perfect-Forwarding/02-Universal-References(Item 24:universal reference 的辨識,本 Item 揭露其底層機制)
- 06-Move-Semantics-And-Perfect-Forwarding/03-Move-Vs-Forward-Usage(Item 25:在 universal reference 上套用
std::forward的慣例) - 02-Deducing-Types/01-Template-Type-Deduction(Item 1:universal reference 的特殊推導規則)
- 02-Deducing-Types/02-Auto-Type-Deduction(Item 2:
auto&&型別生成與 template 推導本質相同) - 02-Deducing-Types/03-Decltype(Item 3:collapsing context 之一的
decltype) - 04-Moving-To-Modern-Cpp/03-Alias-Declarations(Item 9:typedef/alias 與
remove_referencetype trait) - 07-Lambda-Expressions/03-Decltype-Auto-Forwarding(Item 33:generic lambda 中用
decltype+ reference collapsing 完成轉發)