Item 31:避免使用預設捕獲模式 (Avoid Default Capture Modes)
Overview Table
| 捕獲模式 | 語法 | 主要風險 | 建議 |
|---|---|---|---|
| 預設 by-reference | [&] |
被捕獲變數先亡 → dangling reference | 避免;改用明確捕獲 [&x] |
| 預設 by-value | [=] |
隱性捕獲 this 指標 → dangling pointer;誤導人以為 closure 自給自足 (self-contained) |
避免;改用明確捕獲 [x] 或 init capture |
| 明確 by-reference | [&x] |
仍可能懸空,但捕獲清單明確提醒要做生命週期分析 | 可用(需確認 x 壽命) |
| 明確 by-value | [x] |
需 x 為區域變數/參數才能捕獲 |
建議 |
| Init capture (C++14) | [x = expr] |
無預設模式可言 | 捕獲 data member / 搬移物件的最佳解 |
捕獲只作用於 lambda 建立處可見的 non-static 區域變數(含參數)。class data member 與 static storage duration 物件都不可被捕獲——這正是 [=]、[&] 誤導人的根源。
預設 by-reference 捕獲:懸空參考 (Dangling References)
closure 若存活超過被參考的區域變數,內含的 reference 即懸空。
using FilterContainer =
std::vector<std::function<bool(int)>>; // 過濾函式容器(見 Item 9 的 using)
FilterContainer filters;
void addDivisorFilter()
{
auto calc1 = computeSomeValue1();
auto calc2 = computeSomeValue2();
auto divisor = computeDivisor(calc1, calc2); // 區域變數
filters.emplace_back(
[&](int value) { return value % divisor == 0; } // 危險!捕獲 divisor 的 reference
);
} // divisor 析構 → filters 內的 closure 立刻懸空,呼叫即 undefined behavior
- 明確寫成
[&divisor]一樣會懸空,但捕獲清單點名了依賴,提醒你檢查divisor的生命週期;[&]只給你一句籠統的「注意別懸空」。 - 長期而言,明確列出 lambda 依賴的區域變數與參數才是良好的軟體工程。
若 closure 只是當場傳給 STL 演算法(如 std::all_of)、不會被複製或存起來,[&] 其實是安全的。但這種安全很脆弱:lambda 一旦被複製貼上到 closure 可能存活更久的情境(如加入 filters),就會回到懸空地獄,而且捕獲清單中沒有任何提醒。
預設 by-value 捕獲:隱性捕獲 this 指標
[=] 看似把用到的東西都複製進 closure,但data member 不是區域變數,無法被捕獲——實際被複製的是 raw pointer this。
class Widget {
public:
void addFilter() const; // 將過濾函式加入 filters
private:
int divisor; // 供過濾函式使用
};
void Widget::addFilter() const
{
filters.emplace_back(
[=](int value) { return value % divisor == 0; } // 實際捕獲的是 this!
);
}
// 編譯器視同:
// auto currentObjectPtr = this;
// [currentObjectPtr](int value)
// { return value % currentObjectPtr->divisor == 0; }
[](int value){ return value % divisor == 0; }(無捕獲)→ 編譯錯誤:divisor不可用。[divisor](...)明確捕獲 → 編譯錯誤:沒有名為divisor的區域變數可捕獲。- 因此
[=]能編譯的唯一原因就是它偷偷複製了this。
closure 的存活因此綁定該 Widget 物件的生命週期:
doSomeWork()
├─ auto pw = std::make_unique<Widget>(); // 建立 Widget
├─ pw->addFilter(); // closure 存入 this 指標的複本
│ │
│ ▼
│ filters ──> [this*] ──> Widget 物件
└─ 函式結束:unique_ptr 析構 Widget
│
▼
filters ──> [this*] ──> (已析構!) // dangling pointer,UB
即使全程使用 std::make_unique / std::unique_ptr(見 Item 18、21),closure 裡的 this 複本仍是 raw pointer;Widget 一亡,指標即懸空。「現代 C++ 沒有 raw pointer」是錯覺——this 就是。
正確做法:複製 data member 或 C++14 init capture
// 做法一(C++11):先做區域複本,再捕獲複本
void Widget::addFilter() const
{
auto divisorCopy = divisor; // 複製 data member
filters.emplace_back(
[divisorCopy](int value) // 捕獲複本
{ return value % divisorCopy == 0; } // 使用複本
);
}
// 做法二(C++14):generalized lambda capture(init capture,見 Item 32)
void Widget::addFilter() const
{
filters.emplace_back(
[divisor = divisor](int value) // 把 divisor 複製進 closure
{ return value % divisor == 0; } // 使用 closure 內的複本
);
}
- 做法一之下
[=]也能正確運作(會複製divisorCopy),但何必冒險——當初就是預設模式讓你誤捕this。 - init capture 沒有預設模式可用,所以即使在 C++14,「避免預設捕獲模式」的建議依然成立。
static 變數:[=] 假裝自給自足
[=] 的另一缺點:讓人誤以為 closure 與外界隔離。static storage duration 物件(global、namespace scope、class/函式/檔案內 static)可以在 lambda 裡使用,卻不可被捕獲。
void addDivisorFilter()
{
static auto calc1 = computeSomeValue1(); // static
static auto calc2 = computeSomeValue2(); // static
static auto divisor = computeDivisor(calc1, calc2);
filters.emplace_back(
[=](int value) // 什麼都沒捕獲!
{ return value % divisor == 0; } // 直接參照上方的 static 變數
);
++divisor; // 修改 divisor → 所有已加入的 closure 行為跟著改變
}
實務效果等同 by-reference 捕獲,與 [=] 給人的印象完全矛盾。不用預設 by-value 捕獲,就能避免程式碼被這樣誤讀。
Exam/Test Patterns
| 情境關鍵字 | 答案 |
|---|---|
[&] + closure 存活超過區域變數 |
dangling reference → undefined behavior |
[=] 在 member function 內使用 data member |
實際捕獲 this 指標,非 data member |
物件析構後呼叫存下來的 closure(捕獲了 this) |
dangling pointer → UB;智慧指標無法防止 |
| lambda 無捕獲卻直接用 data member | 編譯錯誤:data member 不可捕獲、也不在 scope |
[divisor] 明確捕獲 data member |
編譯錯誤:只能捕獲 non-static 區域變數/參數 |
| C++14 想安全捕獲 data member | init capture:[divisor = divisor](Item 32) |
[=] + lambda 內使用 static 變數 |
什麼都沒捕獲;行為如 by-reference,非自給自足 |
| closure 只當場傳給 STL 演算法、不複製 | [&] 當下安全,但複製貼上後易踩雷,仍建議明確捕獲 |
| 為何偏好明確捕獲清單 | 點名依賴、提醒生命週期分析、避免誤捕 this 與誤讀 |
Related Notes
- 07-Lambda-Expressions/02-Init-Capture:C++14 init capture(generalized lambda capture)是捕獲 data member 複本與搬移物件進 closure 的正解。
- 07-Lambda-Expressions/03-Decltype-Auto-Forwarding:generic lambda 的參數轉發,同章延伸主題。
- 07-Lambda-Expressions/04-Lambdas-Vs-Std-Bind:lambda 與 std::bind 的取捨;捕獲語意在 lambda 中遠比 bind 物件明確。
- 05-Smart-Pointers/01-Unique-Ptr:範例中
std::unique_ptr析構 Widget 導致 closure 內this懸空。 - 05-Smart-Pointers/04-Make-Unique-And-Make-Shared:
std::make_unique的使用背景(Item 21)。 - 06-Move-Semantics-And-Perfect-Forwarding/01-Std-Move-And-Std-Forward:init capture 搭配
std::move的基礎(Item 23)。 - 07-Lambda-Expressions/Practice-Lambda-Expressions:本章練習題。