Lambda Capture by Move
之前的文章稍微提過了用來轉移資料所有權的 std::move
,這次來聊聊怎樣與 lambda 搭配服用。對 C++14 及其之後的標準而言,這再簡單不過了:
#include <cassert>
#include <functional>
#include <iostream>
#include <memory>
#include <utility>
int main()
{
std::unique_ptr<int> x(new int(42));
auto f = [x = std::move(x)](int y) { return *x + y; };
assert(!x);
std::cout << f(3) << std::endl;
return 0;
}
這一段程式碼會把 lambda 外面的 x
的資料所有權轉移給 lambda 裡面的 x
(注意 [x = std::move(x)]
左手邊的 x
指的是 lambda 裡面的 x
,而右手邊的 x
指的是 lambda 外面的 x
)。雖然之前提到過在經由 std::move
轉移所有權之後,存取 lambda 外面 x
一般而言屬於未定義的行為,但因為 std::unique_ptr
的 move assignment 運算子明確定義了在被轉移所有權之後,其內容會變成 nullptr
,所以 assert(!x)
不會有任何問題。另外,因為 f
有了一個不能被複製的 capture,f
本身也不能夠被複製。
如果有 C++14 可以用,那接下來的文章就可以不用看了😛。可惜的是,由於某些因素,我在開發的 Mesos 還沒有全面使用 C++14。如果以 C++11 來編譯上面的程式碼,會得到像下面的錯誤訊息:
warning: initialized lambda captures are a C++14 extension [-Wc++14-extensions]
auto f = [x = std::move(x)](int y) {
^
那麼,在 C++11 裡有什麼替代方案呢?最簡單的當然就是用 std::shared_ptr
代替 std::unique_ptr
了:
std::shared_ptr<int> x(new int(42));
auto f = [x](int y) { return *x + y; };
assert(x);
雖然這不失為一個可行的方法,但從上面的 assert(x)
就可以看到,使用 std::shared_ptr
沒有辦法確保 x
只能在 f
裡面存取。如果程式的正確性倚賴這個前提,這樣寫便顯得有些危險了。即便如此,因為這樣寫起來比下面要講的另一個方法來得易讀,在 Mesos codebase 裡還是有好些地方這樣寫,等到全面用上 C++14 之後再一一改掉。
除了使用 std::shared_ptr
之外,其實還有一種方式在 C++11 裡面做到類似 by-move capture 的功能:透過 std::bind
!如果對傳統 C++ 比較熟稔的話,可能對 std::bind1st
和 std::bind2nd
有點印象。這兩個工具函數的功能是把一個有 n 個參數的函數或 fuction object 的第一個或第二個參數固定住,生成一個 n - 1 個參數的 function object。C++11 拋棄了這兩個工具函數,取而代之的是更加一般化的 std::bind
,可以任意固定 k 個參數並結合 placeholders 生出一個 n - k 個參數的 function object。使用 std::bind
實現 by-move capture 的方式如下:
std::unique_ptr<int> x(new int(42));
auto f = std::bind(
[](const std::unique_ptr<int>& x, int y) { return *x + y; },
std::move(x),
std::placeholders::_1);
assert(!x);
上面這段程式碼中的 lambda 完全沒有任何 capture,而是多宣告了一個新的參數 x
,然後我們再透過 std::bind
把 lambda 外面的 x
的資料所有權轉移到生成的 function object 裡面,當作呼叫 lambda 的參數 x
使用。如此一來,就可以確保 x
只能在 f
裡面存取。
不過,雖然 std::bind
可以在 C++11 裡實現 by-move capture, 但比起前面的寫法,這樣做還是繁瑣許多。而且 std::bind
還有一個小小的問題:它生出來的 function object 和一個普通的 lambda 有些行為不盡相同!不過這要講起來又是一篇文章,所以在這裡先賣個關子,有空再來分享😜。如果可以的話,還是儘快換到 C++14 吧!