Item 24:區分 universal reference 與 rvalue reference (Distinguish Universal References from Rvalue References)

Overview Table

重點 內容
核心問題 原始碼中的 T&& 不一定是 rvalue reference——它有兩種身分
T&& 的兩種身分 rvalue reference(只綁 rvalue)或 universal reference(rvalue 與 lvalue 都能綁)
Universal reference 成立條件 (1) 發生型別推導(type deduction)+ (2) 宣告形式恰好T&&type&&,不能多任何修飾)
兩大出現場景 函式模板參數 template<typename T> void f(T&& param)auto&& 變數宣告
綁定能力 可綁 lvalue / rvalue / const / non-const / volatile——幾乎任何東西
最終身分由誰決定 初始器(initializer):rvalue 初始化 → 對應 rvalue reference;lvalue 初始化 → 對應 lvalue reference
常見破格情形 std::vector<T>&&const T&&、模板類別成員函式的 T&&(如 push_back)→ 全是 rvalue reference
背後真相 universal reference 是一種抽象(abstraction),底層機制是 reference collapsing(Item 28)
別名

本書出版時,C++ 社群已開始將 universal reference 稱為 forwarding reference(C++17 標準採用此名)。兩者是同一概念。

T&& 的兩種意義與判斷流程

T&& 可能是 rvalue reference,也可能是 universal reference;關鍵在於是否發生型別推導形式是否精確為 type&&——兩個條件缺一不可:

                看到 "type&&" 宣告
                        │
                        ▼
          有發生型別推導嗎?(函式模板參數 / auto&&)
                        │
                 ┌──────┴──────┐
                 否            是
                 │             ▼
                 │    宣告形式「恰好」是 T&& 嗎?
                 │    (vector<T>&&、const T&& 都不算)
                 │             │
                 │       ┌────┴────┐
                 │       否        是
                 ▼       ▼         ▼
              rvalue reference   universal reference
              (只綁 rvalue)           │
                              初始器是 lvalue → 表現為 lvalue reference (T&)
                              初始器是 rvalue → 表現為 rvalue reference (T&&)
void f(Widget&& param);          // 無型別推導 → rvalue reference

Widget&& var1 = Widget();        // 無型別推導 → rvalue reference

auto&& var2 = var1;              // auto 推導 + 形式 auto&& → universal reference
                                 // (var1 是 lvalue,故 var2 為 lvalue reference)

template<typename T>
void fvector<T>&& param;  // 形式是 vector<T>&&,非 T&& → rvalue reference

template<typename T>
void f(const T&& param);         // 多了 const → rvalue reference(喪失 universal 資格)

template<typename T>
void f(T&& param);               // 型別推導 + 精確 T&& → universal reference
種類 可綁定對象 主要用途
rvalue referenceWidget&&,無推導) 只有 rvalue 識別「可被 move 的物件」
universal referenceT&& + 推導) rvalue、lvalue、const、非 const、volatile……幾乎任何東西 完美轉發(perfect forwarding)
std::vector<int> v;
f(v);   // 對 fvector<T>&& 而言:錯誤!lvalue 不能綁到 rvalue reference

初始器決定 universal reference 的最終身分

universal reference 是 reference,必須被初始化;初始器是 rvalue 還是 lvalue 決定它「變成」哪種 reference。函式參數的初始器由呼叫端(call site)提供:

template<typename T>
void f(T&& param);       // param 是 universal reference

Widget w;
f(w);                    // 傳入 lvalue → T 推導為 Widget&,
                         // param 型別為 Widget&(lvalue reference)

fmove(w);         // 傳入 rvalue → T 推導為 Widget,
                         // param 型別為 Widget&&(rvalue reference)
初始器 universal reference 的表現 T 的推導結果
lvalue(如 w 對應 lvalue referenceWidget& T = Widget&
rvalue(如 std::move(w)、暫時物件) 對應 rvalue referenceWidget&& T = Widget

「在模板裡」不等於「有型別推導」:push_back vs emplace_back

身處模板中的 T&& 參數不保證是 universal reference——經典對照是 std::vector 的兩個成員函式:

template<class T, class Allocator = allocator<T>>
class vector {
public:
    void push_back(T&& x);        // 形式對,但 T 在 vector 實體化時已固定,
                                  // 呼叫 push_back 時無型別推導 → rvalue reference

    template <class... Args>
    void emplace_back(Args&&... args);  // Args 獨立於 T,每次呼叫都要推導
                                        // → universal reference(參數包)
};

std::vector<Widget> v;   // 實體化後:void push_back(Widget&& x);
                         // 清楚可見是 rvalue-reference-to-Widget
成員函式 呼叫時有型別推導? 參數性質
push_back(T&& x) 否(Tvector<Widget> 實體化早已固定) rvalue reference
emplace_back(Args&&... args) 是(Args 每次呼叫獨立推導) universal reference
名稱不必是 T

只要「形式為 type&& 且該 type 被推導」就是 universal reference——MyTemplateType&&Args&& 都算。重點在形式,不在名稱。

顯式指定模板引數的角落案例

若呼叫端顯式指定模板引數(如 f<Widget>(w)),型別推導不會發生,此為本 Item 刻意忽略的 edge case。此外,「universal reference」整個概念本身就是一個簡化的抽象——底層真相是 reference collapsing,詳見 Item 28。

auto&& universal reference 與 C++14 lambda

auto&& 宣告的變數同樣滿足「推導 + 正確形式」,因此是 universal reference。C++14 起 lambda 可宣告 auto&& 參數,使 universal reference 大量出現:

// C++14:計時任意函式呼叫的 lambda
auto timeFuncInvocation =
    [](auto&& func, auto&&... params)              // func、params 都是
    {                                              // universal reference
        // 啟動計時器
        std::forward<decltype(func)>(func)(          // 呼叫 func
            std::forward<decltype(params)>(params)... // 完美轉發所有參數
        );
        // 停止計時器並記錄耗時
    };
// func:可綁定任何 callable(lvalue 或 rvalue)
// params:universal reference 參數包,可綁定任意數量、任意型別的引數
「幾乎」任何函式,不是「所有」

timeFuncInvocation 能計時「幾乎」任何函式呼叫,並非全部——perfect forwarding 存在失敗案例(braced initializer、0/NULL 作為 null 指標等),詳見 Item 30。std::forward<decltype(...)> 慣用法的原理見 Item 33。

這整個 Item 是一個有用的「抽象」

universal reference 底層的真相是 reference collapsing(Item 28)。但如同牛頓力學之於相對論,這個抽象通常更好用:它讓你準確閱讀程式碼(「這個 T&& 只綁 rvalue,還是什麼都綁?」)、與同事精確溝通,並且是理解 Item 25、26 的前提。

Exam/Test Patterns

情境關鍵字 答案
void f(Widget&& param)param 是什麼 rvalue reference(無型別推導)
template<typename T> void f(T&& param)param 是什麼 universal reference(推導 + 精確 T&& 形式)
auto&& var2 = var1;var2 是什麼 universal referencevar1 是 lvalue → var2 為 lvalue reference
template<typename T> void fvector<T>&& param rvalue reference(形式非精確 T&&);傳 lvalue 會編譯錯誤
template<typename T> void f(const T&& param) rvalue referenceconst 取消 universal 資格)
universal reference 以 lvalue 初始化 表現為 lvalue referenceT 推導為 Widget&
universal reference 以 rvalue 初始化 表現為 rvalue referenceT 推導為 Widget
vector<T>::push_back(T&& x) 是 universal reference 嗎 不是——實體化時 T 已固定,呼叫時無推導
emplace_back(Args&&... args) 是 universal reference 嗎 ——Args 每次呼叫獨立推導,且形式正確
universal reference 的別名 forwarding reference(轉發參考)
universal reference 的底層機制 reference collapsing(Item 28);universal reference 是實用的抽象
universal reference 該搭配哪個轉型工具 std::forward(rvalue reference 才用 std::move,見 Item 25)