c++ lambda內std::move失效問題的思考

最近在寫C++時,有這樣一個代碼需求:在lambda中,將一個捕獲參數move給另一個變量。 看似一個很簡單常規的操做,然而這個move動做卻沒有生效。c++

具體代碼以下:bash

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時並無按預期調用移動構造函數,而是調用了拷貝構造函數。ui

爲何會形成這個問題呢, 咱們須要結合std::movelambda的原理看下。(最終的解決方案能夠直接看這裏spa

std::move的本質

對於std::move,有兩點須要注意:code

  1. std::move中到底作了什麼事情
  2. std::move是否能夠保證數據必定能移動成功

對於第二點來講,答案顯然是不能。這也是本文的問題所在。那麼std::move其實是作了什麼事情呢?對象

對於std::move,其實現大體以下:rem

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本質上是將對象強制轉換爲了右值引用。get

那麼,爲何咱們一般使用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運行。

【原文首發於騰訊雲加社區, 同步於我的博客

參考

相關文章
相關標籤/搜索