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

  • 2019 年 10 月 10 日
  • 筆記

博客:www.cyhone.com

公眾號:編程沉思錄


最近在寫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::movelambda的原理看下。(最終的解決方案可以直接看文章末尾)

std::move的本質

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

  1. std::move中到底做了什麼事情
  2. 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運行。

參考