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 機制 lvalueT 推導為 T&(lvalue reference);傳 rvalueT 推導為非 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&&
Important

編碼機制不對稱:lvalue 編碼為 lvalue reference,rvalue 卻編碼為「非 reference」(而不是 rvalue reference)。這個不對稱正是 std::forward 能運作的關鍵。

Warning

此編碼只發生在引數初始化 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&)
Warning

typedef / alias 的名稱不保證語意:上例 RvalueRefToTWidget<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
Warning

上述實作為教學用簡化版,並非完全符合 Standard(省略了部分介面細節,例如處理 rvalue 引數的 overload 與 constexpr/noexcept 修飾),但行為模型正確。

Tip

這解釋了為什麼呼叫時必須寫 std::forward<T>(param)——顯式提供 T 才能把「引數原本是 lvalue 還是 rvalue」的編碼資訊帶進來。

Universal Reference 的真相

Universal reference 不是新的 reference 種類,它其實就是 rvalue reference,只是出現在同時滿足兩個條件的 context:

  1. 型別推導會區分 lvalue 與 rvalue:型別 T 的 lvalue 推導為 T&,rvalue 推導為 T
  2. 會發生 reference collapsing
auto&& w1 = w;                 // lvalue:auto = Widget&
                               // Widget& && → 塌縮 → w1 是 Widget&(lvalue reference)
auto&& w2 = widgetFactory();   // rvalue:auto = Widget
                               // 無塌縮 → w2 是 Widget&&(rvalue reference)
Tip

「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?」 TT 為非 reference(編碼 rvalue)→ static_cast<T&&> 產生 rvalue reference;T 為 lvalue reference → 塌縮後轉型無作用
「universal reference 是什麼?」 「型別推導區分 lvalue/rvalue + 發生 reference collapsing」的 context 中的 rvalue reference,不是新的 reference 種類
Widget<int&>typedef T&& RR 是?」 int&int& && 塌縮)——名為 RvalueRef 實為 lvalue reference