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
例外:立即使用且不被複製的 closure

若 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; }

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 內的複本
  );
}

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 與誤讀