Item 33:對 auto&& 參數使用 decltype 來 std::forward (Use decltype on auto&& Parameters to std::forward Them)

Overview Table

主題 重點
核心結論 在 generic lambda 中要完美轉發 auto&& 參數時,寫 std::forward<decltype(param)>(param)
generic lambda C++14 特性:lambda 參數可用 auto,closure class 的 operator()template
問題根源 lambda 內沒有可引用的模板參數 T,無法照慣例寫 std::forward<T>
解法原理 decltype(x):lvalue 傳入 → lvalue reference;rvalue 傳入 → rvalue reference
為何可行 以 rvalue reference 型別實例化 std::forward,經 reference collapsing 後結果與傳統的非參考型別完全相同
變參版本 [](auto&&... params) { fforward<decltype(params)>(params)...; }

Generic Lambda 的本質:templated operator()

C++14 允許 lambda 參數使用 autogeneric lambda),其實作方式是讓 closure class 的 operator() 成為模板:

auto f = [](auto x){ return func(normalize(x)); };

// 編譯器生成的 closure class 大致如下:
class SomeCompilerGeneratedClassName {
public:
  template<typename T>            // operator() 是模板
  auto operator()(T x) const      // auto 回傳型別(見 Item 3)
  { return func(normalize(x)); }
  …                               // 其他 closure class 功能
};

上面的寫法有一個缺陷:若 normalize 對 lvalue 與 rvalue 有不同處理,這個 lambda 永遠傳 lvalue(具名參數 x 是 lvalue),即使呼叫者傳入的是 rvalue,rvalue 資訊也會遺失。

正確做法需要兩步(概念上就是完美轉發):

  1. x 改成 universal referenceauto&& x(見 Item 24)
  2. std::forward 轉發給 normalize(見 Item 25)
auto f = [](auto&& x)
         { return funcforward<???>(x)); };
// 問題:??? 該填什麼?lambda 裡沒有 T 可以用!

closure class 內雖然有模板參數 T,但在 lambda 本體中無法引用它——這就是本 Item 要解決的問題。

解法:用 decltype(x) 判斷傳入的是 lvalue 還是 rvalue

Item 28 說明:lvalue 傳給 universal reference 參數 → 參數型別為 lvalue reference;rvalue 傳入 → 參數型別為 rvalue reference。因此檢查 x 的型別即可得知傳入值類別,而 decltype 正是做這件事的工具(見 Item 3):

傳入引數 decltype(x) 產生 std::forward 慣例期望的型別 是否相符
lvalue(如 Widget 左值) Widget&(lvalue reference) lvalue reference(Widget& 完全符合慣例
rvalue(如 Widget{} Widget&&(rvalue reference) 非參考型別(Widget 不符慣例,但結果相同(見下節)

為何 decltype(x) 傳給 std::forward 依然正確:reference collapsing

Item 28 的 C++14 版 std::forward 範例實作:

template<typename T>                        // 位於 namespace std
T&& forward(remove_reference_t<T>& param)
{
  return static_cast<T&&>(param);
}

比較「慣例寫法 T = Widget」與「decltype 產生的 T = Widget&&」兩種實例化:

T = Widget(慣例:非參考)             T = Widget&&(decltype 對 rvalue 的結果)
────────────────────────────          ─────────────────────────────────────────
Widget&& forward(Widget& param)       Widget&& && forward(Widget& param)   ← 摺疊前
{                                     { return static_cast<Widget&& &&>(param); }
  return static_cast<Widget&&>(param);              │
}                                                   ▼ reference collapsing:&& + && → &&
                                      Widget&& forward(Widget& param)      ← 摺疊後
                                      { return static_cast<Widget&&>(param); }

              兩者實例化結果【完全相同】 ⇒ decltype(x) 可安全交給 std::forward

所以無論 lvalue 或 rvalue,std::forward<decltype(x)>(x) 都給出正確行為。完成品:

auto f =
  [](auto&& param)
  {
    return
      funcforward<decltype(param)>(param));
  };

C++14 lambda 也支援變參(variadic),可轉發任意數量的參數:

auto f =
  [](auto&&... params)   // 參數包:universal reference 版本
  {
    return
      funcforward<decltype(params)>(params)...);
  };                     // 對每個參數展開 forward
例外與限制

  • C++11 不適用:generic lambda(auto 參數)與變參 lambda 皆為 C++14 特性;C++11 中若需要相同效果,必須手寫含模板 operator() 的 functor class(此時有 T 可用,照慣例寫 std::forward<T> 即可)。
  • decltype(x) 對 rvalue 產生 rvalue reference」的前提是 x具名的 auto&& 參數decltype 用在其他表達式(如加括號的 (x))有不同規則(見 Item 3),不可混為一談。
  • 若 lambda 內多次使用同一參數,與一般完美轉發相同:只能在最後一次使用std::forward(見 Item 25),否則物件可能已被搬移走。

記憶口訣

lambda 裡沒有 T → 用 decltype(param) 頂替:std::forward<decltype(param)>(param)

Exam/Test Patterns

情境關鍵字 答案
generic lambda 想完美轉發 auto&& 參數,std::forward<?> 填什麼 decltype(param),即 std::forward<decltype(param)>(param)
為什麼 lambda 內不能寫 std::forward<T> closure class 的 operator() 雖是模板,但其模板參數 T 在 lambda 本體中無法引用
decltype(x) 對 rvalue 給出 Widget&&(非慣例的非參考型別),為何仍正確 std::forward<Widget&&>reference collapsing&& + && → &&)後與 std::forward<Widget> 實例化結果完全相同
lambda 參數 auto&& x 收到 lvalue 時 decltype(x) 是什麼 lvalue reference(如 Widget&),符合 std::forward 慣例
[](auto x){ return normalize(x); } 的問題 x 是具名參數(lvalue),rvalue 資訊遺失,normalize 永遠收到 lvalue
轉發任意數量參數的 lambda 怎麼寫 [](auto&&... params){ fforward<decltype(params)>(params)...; }(C++14)
generic lambda 是哪個標準的特性 C++14(closure class 的 operator() 為 template)