Lambda 運算式練習題 (Practice - Lambda Expressions)
Related Concepts
- 07-Lambda-Expressions/01-Avoid-Default-Capture-Modes
- 07-Lambda-Expressions/02-Init-Capture
- 07-Lambda-Expressions/03-Decltype-Auto-Forwarding
- 07-Lambda-Expressions/04-Lambdas-Vs-Std-Bind
| 關鍵字 | 答案 |
|---|---|
[&] + closure 存活超過區域變數 |
dangling reference → undefined behavior |
[=] 在 member function 內用 data member |
實際捕獲 this 指標,非 data member |
[=] + lambda 內使用 static 變數 |
什麼都沒捕獲;行為如 by-reference |
| C++14 安全捕獲 data member | init capture:[divisor = divisor] |
| move-only 物件放進 closure | C++14 init capture:[pw = std::move(pw)] |
| C++11 模擬 move capture | 手寫 class 或 std::bind+lvalue reference 參數 |
generic lambda 完美轉發 auto&& |
std::forward<decltype(param)>(param) |
rvalue 時 decltype(x) 為 Widget&& 仍正確 |
reference collapsing:&& + && → && |
std::bind 引數求值時機 |
bind 當下求值;延遲需巢狀 std::bind |
| bind object 儲存 / 呼叫傳遞 | 一律以值儲存(std::ref 例外);呼叫一律以參考 |
| bind 重載函式 | 名稱歧義編譯錯誤;需 static_cast 成函式指標 |
C++11 std::bind 僅存用例 |
move capture 模擬、綁定 polymorphic function object |
Question 1 - 預設 by-reference 捕獲的懸空風險 [recall]
addDivisorFilter()內以[&](int value){ return value % divisor == 0; }將 lambda 加入全域的filters容器,其中divisor是區域變數;函式返回後呼叫該 filter 會發生什麼事?
Undefined behavior。closure 內存的是 divisor 的 reference,addDivisorFilter 返回時 divisor 析構,reference 立即懸空(dangling reference)——這個 filter「一出生就死了」。
改成明確的 [&divisor] 一樣會懸空,但捕獲清單點名了依賴,提醒你做生命週期分析;[&] 沒有任何提醒。
Question 2 - [=] 在 member function 內的真相 [recall]
Widget::addFilter() const內以[=](int value){ return value % divisor == 0; }使用 data memberdivisor,[=]複製進 closure 的到底是什麼?
複製的是 this 指標,不是 divisor。捕獲只作用於 lambda 建立處可見的 non-static 區域變數(含參數),data member 不可被捕獲;編譯器把 divisor 視同 this->divisor,故 [=] 實際捕獲的是 raw pointer this 的複本。closure 的存活因此綁定該 Widget 物件的生命週期。
Question 3 - [=] 與 static 變數 [recall]
函式內宣告
static auto divisor = ...;,lambda 以[=]加入filters後函式每次結尾執行++divisor;已加入的 closure 行為會改變嗎?為什麼?
會改變。static storage duration 物件可在 lambda 內使用但不可被捕獲——這個 lambda 沒有用到任何 non-static 區域變數,所以 [=] 其實什麼都沒捕獲,lambda 直接參照那個 static 變數。
實務效果等同 by-reference 捕獲,與 [=]「自給自足」的印象完全矛盾,這正是避免預設 by-value 捕獲的理由之一。
Question 4 - 修正 data member 的捕獲 [application]
請給出兩種修正
Widget::addFilter() const的寫法(C++11 一種、C++14 一種),讓 closure 真正持有divisor的複本而非this。
// C++11:先做區域複本,再明確捕獲複本
auto divisorCopy = divisor;
filters.emplace_back(
[divisorCopy](int value) { return value % divisorCopy == 0; });
// C++14:init capture(generalized lambda capture)
filters.emplace_back(
[divisor = divisor](int value) { return value % divisor == 0; });
C++11 做法之下 [=] 也能運作(複製到的是 divisorCopy),但預設模式正是當初誤捕 this 的元兇,不建議冒險。
Question 5 - 智慧指標救不了懸空的 this [analysis]
doSomeWork()以std::make_unique<Widget>()建立pw,呼叫pw->addFilter()(內部用[=]捕獲)後返回;全程只用智慧指標,為何filters內仍出現 dangling pointer?請追蹤完整過程。
追蹤:(1) addFilter 的 [=] 捕獲的是 this 的複本(raw pointer),closure 透過它存取 divisor;(2) closure 被存入生命週期更長的全域 filters;(3) doSomeWork 返回時 std::unique_ptr 析構 Widget,closure 內的 this 複本隨即懸空——之後呼叫該 filter 即 UB。
智慧指標只管理 Widget 本身的生命週期,管不到 closure 裡偷偷複製的 raw pointer this;「現代 C++ 沒有 raw pointer」是錯覺。另注意:改寫成 [divisor] 明確捕獲也無法編譯——沒有名為 divisor 的區域變數可捕獲,這揭露了 [=] 能編譯的唯一原因就是它捕獲了 this。
Question 6 - init capture 兩側的 scope [recall]
auto func = [pw = std::move(pw)] { return pw->isValidated() && pw->isArchived(); };中,=左右兩個pw分別是什麼、位於哪個 scope?
左側 pw 是 closure class 的資料成員,scope 屬於 closure class;右側 pw 是 lambda 定義處 scope 的區域變數(被 std::move 後移入成員)。
lambda 本體內的 pw 一律指 closure 資料成員(左側那個)。
Question 7 - 為何叫 generalized lambda capture [recall]
init capture 為什麼又稱 generalized lambda capture?它能做到哪件 C++11 capture 做不到的事、又有什麼唯一做不到的事?
因為它可以捕獲任意運算式的結果,例如 [pw = std::make_unique<Widget>()] 直接以 make_unique 的回傳值初始化 closure 成員——C++11 capture 無法捕獲運算式結果。
唯一做不到的是 default capture mode,但 Item 31 本來就建議避免預設捕獲,實務上不構成問題。
Question 8 - C++11 模擬 move capture [application]
你只有 C++11 編譯器,想把填好資料的
std::vector<double> data「move 進 closure」;請用std::bind寫出模擬版,並說明 lambda 參數的宣告方式。
auto func = std::bind(
[](const std::vector<double>& data) // lvalue reference 參數
{ /* 使用 data */ },
std::move(data)); // rvalue → move 建構進 bind object
兩步驟:物件先 move 進 bind object,lambda 再以 lvalue reference 參數存取它。參數加 const 是為了模擬 closure 成員在 operator()(預設 const)內的唯讀行為;若 lambda 為 mutable 則應省略 const。另一種 C++11 替代方案是手寫 class,建構子以 rvalue reference 收參數並 std::move 進資料成員。
Question 9 - bind object 的儲存規則 [recall]
std::bind收到 lvalue 引數與 rvalue 引數時分別如何存入 bind object?bind object 與其內含 closure 的生命週期關係為何?
lvalue → copy 建構;rvalue → move 建構。std::move(data) 是 rvalue,故被 move 進 bind object——這正是 C++11 模擬 move capture 的關鍵。
bind object 儲存第一個引數(closure)的副本,因此 closure 與 bind object 生命週期相同,可把 bind object 內的物件當成「在 closure 裡」看待。
Question 10 - generic lambda 的完美轉發 [recall]
auto f = [](auto&& x){ return funcforward<???>(x)); };中???應填什麼?為何不能照慣例寫std::forward<T>?
填 decltype(x),即 std::forward<decltype(x)>(x);變參版為 [](auto&&... params){ fforward<decltype(params)>(params)...); }。
closure class 的 operator() 雖是 template,但其模板參數 T 在 lambda 本體中無法引用,所以沒有 T 可寫——只能用 decltype 從參數型別推知傳入的是 lvalue(→ lvalue reference)還是 rvalue(→ rvalue reference)。
Question 11 - decltype 給出 rvalue reference 為何仍正確 [analysis]
傳 rvalue 給
auto&& x時decltype(x)產生Widget&&而非慣例的非參考型別Widget;請由std::forward的實作推導為何std::forward<Widget&&>與std::forward<Widget>結果相同。
std::forward 實作為 template<typename T> T&& forward(remove_reference_t<T>& param) { return static_cast<T&&>(param); }。
以 T = Widget&& 實例化時,回傳型別與 cast 目標是 Widget&& &&;套用 reference collapsing 規則(rvalue reference to rvalue reference → 單一 rvalue reference)後得 Widget&&——與 T = Widget 的實例化完全相同。
而 lvalue 傳入時 decltype(x) 給出 lvalue reference,本來就符合慣例;故兩種值類別下 std::forward<decltype(x)>(x) 皆正確。
Question 12 - std::bind 的引數求值時機 [recall]
std::bind(setAlarm, steady_clock::now() + 1h, _1, 30s)想設定「呼叫後一小時響鈴」,錯在哪?如何修正?
steady_clock::now() + 1h 是傳給 std::bind 的引數,在呼叫 std::bind 當下就求值並存入 bind object——鬧鐘變成「bind 後一小時」而非「呼叫 setAlarm 後一小時」。
延遲求值需巢狀 bind:std::bind(setAlarm, std::bindplus<>(), steady_clock::now(), 1h), _1, 30s(C++11 還得寫 std::plus<steady_clock::time_point>)。lambda 版把運算式寫在 body 內,呼叫時才求值,天生正確。
Question 13 - C++11 僅存的 bind 用例 [recall]
Item 34 主張偏好 lambda,但承認
std::bind在 C++11 有兩個合理用例;是哪兩個?C++14 分別被什麼取代?
(1) Move capture 模擬——C++14 由 init capture([x = std::move(x)])取代;(2) 綁定 polymorphic function object(含模板化 operator() 的物件,靠 bind object 的 perfect forwarding 接受任意型別引數)——C++14 由 auto 參數的 generic lambda 取代。
因此 C++14 起沒有任何合理的 std::bind 使用情境。
Question 14 - 重載函式與 bind 的歧義 [application]
setAlarm新增了四參數重載(多了Volume)後,原本的std::bind(setAlarm, ...)無法編譯而 lambda 版照常運作;請解釋原因並寫出讓 bind 編譯的修法。
lambda body 內是一般函式呼叫,overload resolution 自動選出三參數版本;std::bind 只拿到函式名稱,無法判斷要綁哪個重載 → 歧義編譯錯誤。修法是 cast 成特定函式指標型別:
using SetAlarm3ParamType = void(*)(Time, Sound, Duration);
auto setSoundB = std::bind(static_cast<SetAlarm3ParamType>(setAlarm),
std::bindplus<>(), steady_clock::now(), 1h,
_1, 30s);
附帶代價:bind object 透過函式指標呼叫 setAlarm,編譯器較難 inline;lambda 版是直接呼叫、容易 inline,可能產生更快的程式碼。
| 模式 | 一句話結論 |
|---|---|
| Item 31 | 避免 [&] / [=]:前者懸空 reference,後者偷捕 this、又對 static 變數假裝自給自足;明確捕獲點名依賴 |
| Item 31 | 捕獲只作用於 non-static 區域變數(含參數);data member 與 static 物件都不可捕獲 |
| Item 32 | C++14 init capture([pw = std::move(pw)])把物件 move 進 closure;左側是 closure 成員、右側在定義處 scope 求值 |
| Item 32 | C++11 模擬:手寫 class,或 move 進 bind object 再以 lvalue reference 參數傳給 lambda |
| Item 33 | lambda 裡沒有 T → 用 std::forward<decltype(param)>(param);rvalue 的 decltype 給 && 經 reference collapsing 後結果相同 |
| Item 34 | lambda 比 std::bind 更可讀(求值時機、儲存/傳遞方式皆明確)、更能處理重載、更易 inline;C++14 起 bind 無合理用例 |