c++ lambda內std::move失效問題的思考
- 2019 年 10 月 10 日
- 筆記
公眾號:編程沉思錄
最近在寫C++時,有這樣一個代碼需求:在lambda中,將一個捕獲參數move給另外一個變量。
看似一個很簡單常規的操作,然而這個move動作卻沒有生效。
具體代碼如下:
std::vector<int> vec = {1,2,3}; auto func = [=](){ auto vec2 = std::move(vec); std::cout << vec.size() << std::endl; // 輸出:3 std::cout << vec2.size() << std::endl; // 輸出:3 };
代碼可在wandbox運行。
我們期望的是,將對變量vec
調用std::move後,數據將會移動至變量vec2
, 此時vec
裏面應該沒有數據了。但是通過打印vec.size()
發現vec中的數據並沒有按預期移走。
這也就意味着,構造vec2時並沒有按預期調用移動構造函數,而是調用了拷貝構造函數。
為什麼會造成這個問題呢, 我們需要結合std::move
和lambda
的原理看下。(最終的解決方案可以直接看文章末尾)
std::move的本質
對於std::move,有兩點需要注意:
- std::move中到底做了什麼事情
- std::move是否可以保證數據一定能移動成功
對於第二點來說,答案顯然是不能。這也是本文的問題所在。那麼std::move實際上是做了什麼事情呢?
對於std::move,其實現大致如下:
template<typename T> decltype(auto) move(T&& param) { using ReturnType = remove_reference_t<T>&&; return static_cast<ReturnType>(param); }
從代碼可以看出,std::move本質上是調用了static_cast做了一層強制轉換,強制轉換的目標類型是remove_reference_t<T>&&
,remove_reference_t是為了去除類型本身的引用,例如左值引用。總結來說,std::move本質上是將對象強制轉換為了右值引用。
那麼,為什麼我們通常使用std::move實現移動語義,可以將一個對象的數據移給另外一個對象?
這是因為std::move配合了移動構造函數使用,本質上是移動構造函數起了作用。移動構造函數的一般定義如下:
class A{ public: A(A &&); };
可以看到移動構造函數的參數就是個右值引用A&&
,因此 A a = std::move(b);
, 本質上是先將b強制轉化了右值引用A&&
,
然後觸發了移動構造函數,在移動構造函數中,完成了對象b的數據到對象a的移動。
那麼,在哪些情況下,A a = std::move(b);
會失效呢?
顯然是,當std::move強轉後的類型不是A&&
,這樣就不會命中移動構造函數。
例如:
const std::string str = "123" std::string str2(std::move(str));
這個時候,對str對象調用std::move
,強轉出來的類型將會是const string&&
, 這樣移動構造函數就不會起作用了,但是這個類型卻可以令複製構造函數生效。
結合本文最初的問題,在lambda中move沒有生效,顯然也是std::move強轉的類型不是std::vector<int>&&
, 才導致了沒有move成功。
那麼,為什麼會出現這個問題呢,我們需要理解下lambda的工作原理。
lambda閉包原理
對於c++的lambda,編譯器會將lambda轉化為一個獨一無二的閉包類。而lambda對象最終會轉化成這個閉包類的對象。
對於本文最初的這個lambda來說,最終實際上轉化成了這麼一個類型
// 轉換前 auto func = [=](){ auto vec2 = std::move(vec); }; // 轉換後 class ClosureFunc{ public: void operator() const{ auto vec2 = std::move(vec); }; private: std::vector<int> vec; }; ClosureFunc func;
這裡需要注意, lambda的默認行為是,生成的閉包類的**operator()
**默認被const修飾。
那麼這裡問題就來了,當調用operator()
時, 該閉包類所有的成員變量也是被const修飾的,此時對成員變量調用std::move
將會引發上文提到的,強轉出來的類型將會是**const string&&
**,同時,移動構造函數將不會被匹配到。
我們最初的問題lambda中std::move失效的問題,也是因為這個原因。但這個也很符合const函數的語義: const函數是不能修改成員變量的值。
解決方案
那麼,這個應該怎麼解決呢?答案是mutable
。即在lambda尾部聲明一個mutable,如下:
auto func = [=]() mutable{ auto vec2 = std::move(vec); };
這樣編譯器生成的閉包類的operator()
將會不帶const了。我們的std::move也可以正常轉換,實現移動語義了。
std::vector<int> vec = {1,2,3}; auto func = [=](){ auto vec2 = std::move(vec); std::cout << vec.size() << std::endl; // 輸出:0 std::cout << vec2.size() << std::endl; // 輸出:3 };
代碼可以在wandbox運行。
參考
- Lambda 表達式-cppreference
- Effective Modern c++
- 關於C++右值及std::move()的疑問?