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 參數使用 auto(generic 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 資訊也會遺失。
正確做法需要兩步(概念上就是完美轉發):
x改成 universal reference:auto&& x(見 Item 24)- 用
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 傳入:
decltype(x)= lvalue reference,本來就符合std::forward慣例。 - rvalue 傳入:
decltype(x)= rvalue reference,雖不符慣例,但經 reference collapsing 後與傳非參考型別產生一模一樣的實例化結果。
所以無論 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) |
Related Notes
- 07-Lambda-Expressions/02-Init-Capture — 同章兄弟筆記:Item 32 init capture 將物件 move 進 closure;與本 Item 同為 C++14 對 lambda 的強化
- 07-Lambda-Expressions/04-Lambdas-Vs-Std-Bind — 同章兄弟筆記:Item 34 中 C++14 的
auto參數正是 lambda 全面取代std::bind的關鍵之一 - 06-Move-Semantics-And-Perfect-Forwarding/06-Reference-Collapsing — Item 28:本 Item 正確性的理論基礎(universal reference 推導與
&& + && → &&摺疊規則) - 06-Move-Semantics-And-Perfect-Forwarding/03-Move-Vs-Forward-Usage — Item 25:universal reference 應搭配
std::forward,且僅在最後一次使用時轉發 - 06-Move-Semantics-And-Perfect-Forwarding/02-Universal-References — Item 24:
auto&&參數即 universal reference - 02-Deducing-Types/03-Decltype — Item 3:
decltype的規則(本 Item 用它取得參數的參考型別)