Item 32:使用 init capture 將物件移入 Closure (Use Init Capture to Move Objects into Closures)

Overview Table

主題 重點
問題 C++11 的 capture 只有 by-value / by-reference,無法把 move-only 物件std::unique_ptrstd::future或「拷貝貴、移動便宜」的物件 move 進 closure
C++14 解法 init capture(又名 generalized lambda capture):[pw = std::move(pw)],可指定 closure 成員名稱+任意初始化運算式
語法本質 = 左邊是 closure class 的資料成員名;右邊運算式在 lambda 定義處的 scope 求值
能力邊界 init capture 幾乎能做到 C++11 capture 的一切+更多;唯一做不到的是 default capture mode(Item 31 本來就建議避免)
C++11 替代方案 1 手寫一個 class:建構子以 rvalue reference 收參數並 std::move 進資料成員
C++11 替代方案 2 std::bind+lambda:物件先 move 進 bind object,再以 lvalue reference 參數傳給 lambda

核心概念一:init capture 語法與兩側 scope

init capture 讓你同時指定:(1) closure class 中資料成員的名字(2) 初始化該成員的運算式

class Widget {
public:
  bool isValidated() const;
  bool isArchived() const;
  // ...
};

auto pw = std::make_unique<Widget>();   // 建立 Widget(見 Item 21)

// ... 設定 *pw ...

auto func = [pw = std::move(pw)]        // 建立 closure 成員 pw,
            { return pw->isValidated()  // 以 std::move(pw) 初始化
                   && pw->isArchived(); };

= 兩側的 scope 不同,這是 init capture 最容易考的觀念:

[ pw            =            std::move(pw) ]
  │                                     │
  │ 左側:closure class 的 scope        │ 右側:lambda「定義處」的 scope
  │ → 宣告 closure 的資料成員 pw        │ → 這個 pw 指外面的區域變數 pw
  │                                     │
  └── lambda 本體中的 pw 一律指 ────────┘   閉包成員(左側那個)
      closure 資料成員

若不需要先「設定 *pw」,區域變數可完全省略,直接以運算式結果初始化 closure 成員——這正是 C++11 capture 做不到的事(C++11 無法捕獲運算式的結果),因此 init capture 又稱 generalized lambda capture

auto func = [pw = std::make_unique<Widget>()]   // closure 成員直接由
            { return pw->isValidated()          // make_unique 的結果
                   && pw->isArchived(); };      // 初始化
init capture 的唯一限制

init capture 無法表達 default capture mode(沒有 [=...] / [&...] 的 init 版本)。不過 Item 31 本來就建議避免 default capture,所以實務上不構成問題。另外,對於 C++11 capture 就能處理的情境,init capture 語法較囉嗦,直接用傳統 capture 完全合理。

核心概念二:C++11 模擬法(一)——手寫 class

lambda 只是「讓編譯器產生一個 closure class 並建立其物件」的簡寫;lambda 能做的,手寫 class 都能做

class IsValAndArch {                        // "is validated and archived"
public:
  using DataType = std::unique_ptr<Widget>;

  explicit IsValAndArch(DataType&& ptr)     // 以 rvalue reference 收參數
  : pwmove(ptr) {}                   // move 進資料成員(見 Item 25)

  bool operator()() const
  { return pw->isValidated() && pw->isArchived(); }

private:
  DataType pw;
};

auto func = IsValAndArchmake_unique<Widget>();  // C++11 可行

打字量較多,但語意與 C++14 init capture 版本完全等價。

核心概念三:C++11 模擬法(二)——std::bind

想留在 lambda 世界,可用 std::bind 模擬 move capture,步驟:

  1. 把要捕獲的物件 move 進 std::bind 產生的 function object(bind object)
  2. 給 lambda 一個 lvalue reference 參數指向那個「被捕獲」的物件。
std::vector<double> data;                   // 想 move 進 closure 的物件
// ... 填入資料 ...

// C++14:直接 init capture
auto func14 = [data = std::move(data)]
              { /* 使用 data */ };

// C++11:std::bind 模擬
auto func11 = std::bind(
    [](const std::vector<double>& data)     // 參數對應被捕獲物件
    { /* 使用 data */ },
    std::move(data)                         // rvalue → move 建構進 bind object
);

資料流:

std::move(data) ──(move 建構)──> bind object 內的 data 副本
                                       │
呼叫 func11() ──> bind object 的 operator() 以 lvalue reference
                  把內部副本傳給 lambda 的參數 data

關鍵細節(皆為考點):

細節 說明
bind object 如何存引數 lvalue 引數 → copy 建構;rvalue 引數 → move 建構std::move(data) 是 rvalue,故被 move 進 bind object
lambda 參數為何是 lvalue reference bind object 內的副本本身是 lvalue(即使它是被 move 建構出來的),所以參數不是 rvalue reference
為何加 const closure class 的 operator() 預設為 const,closure 成員在 lambda 本體內視同 const;bind object 內的副本卻不是 const,加 const& 才能模擬同樣的唯讀行為
生命週期 bind object 儲存其第一個引數(closure)的副本,closure 與 bind object 生命週期相同,因此可把 bind object 內的物件當成「在 closure 裡」看待
mutable lambda 的例外

若 lambda 宣告為 mutable,其 closure class 的 operator()不是 const,此時模擬版 lambda 的參數應改為非 const 的 lvalue reference:

auto func = std::bind(
    []vector<double>& data mutable   // mutable 版省略 const
    { /* 修改 data */ },
    std::move(data));
與 Item 34 的關係

Item 34 主張「偏好 lambda 勝過 std::bind」,但 C++11 的 move capture 模擬正是 std::bind 少數合理的使用場景之一;到了 C++14,init capture 讓這個場景也消失了。

Exam/Test Patterns

情境關鍵字 答案
「想把 std::unique_ptr / std::future 放進 closure」 C++14 用 init capture[pw = std::move(pw)];C++11 無原生支援
[pw = std::move(pw)] 兩個 pw 各是什麼?」 左:closure class 資料成員;右:lambda 定義處 scope 的區域變數
「capture 運算式的結果(如 make_unique 回傳值)」 C++11 capture 做不到;C++14 init capture 可以 → 故稱 generalized lambda capture
「init capture 做不到什麼?」 default capture mode(且 Item 31 建議本來就別用)
「C++11 如何模擬 move capture?」 (1) 手寫 class(建構子 std::move 進成員);(2) std::bind+lvalue reference 參數的 lambda
「bind object 對 lvalue / rvalue 引數分別做什麼?」 lvalue → copy 建構;rvalue → move 建構
「bind 模擬版中 lambda 參數為何是 const&?」 closure 的 operator() 預設 const,成員視同唯讀;const& 重現此行為(mutable 時省略 const
「bind object 與 closure 的生命週期」 相同——bind object 存有 closure 的副本,故可視 bind 內物件如同在 closure 中