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_ptr、std::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 無法表達 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,步驟:
- 把要捕獲的物件 move 進
std::bind產生的 function object(bind object); - 給 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 裡」看待 |
若 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 主張「偏好 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 中 |
Related Notes
- 07-Lambda-Expressions/01-Avoid-Default-Capture-Modes — Item 31:init capture 無法表達 default capture mode,而 default capture 本身就該避免;成員變數捕獲也可用
[divisor = divisor]解決 - 07-Lambda-Expressions/04-Lambdas-Vs-Std-Bind — Item 34:C++11 的 move capture 模擬是
std::bind少數合理用途;C++14 起被 init capture 取代 - 06-Move-Semantics-And-Perfect-Forwarding/01-Std-Move-And-Std-Forward — Item 23:
std::move(pw)產生 rvalue,是 init capture 移動語意的基礎 - 05-Smart-Pointers/04-Make-Unique-And-Make-Shared — Item 21:範例中以
std::make_unique建立被捕獲的std::unique_ptr - 05-Smart-Pointers/01-Unique-Ptr — Item 18:move-only 型別
std::unique_ptr正是需要 move capture 的典型對象 - 07-Lambda-Expressions/Practice-Lambda-Expressions — 本章練習題