[英]Not sure to understand the advantage of the move constructor (or how it works or use it)
我最近在SE上發布了一個關於下面代碼的問題,因為它產生了編譯錯誤。 當你實現移動構造函數或移動賦值運算符時,有人會回答這個問題,然后刪除默認的復制構造函數。 他們還建議我然后需要使用std::move()
來實現這樣的工作:
Image src(200, 200);
Image cpy = std::move(src);
現在這對我有意義,因為在這種情況下你想要使用移動賦值運算符或移動構造函數的事實必須明確。 這個例子中的src
是一個左值,沒有什么可以告訴編譯器,除非你用std::move
明確表達它,否則你實際上想要將它的內容移動到cpy
。 但是,我對此代碼有更多問題:
Image cpy = src + src
我沒有為下面的operator +
副本,但它是一個簡單的類型的重載運算符:
Image operator + (const Image &img) const {
Image tmp(std::min(w, img.w), std::min(h, img.h));
for (int j = 0; j < tmp.h; ++j) {
for (int i = 0; i < tmp.w; ++i) {
// accumulate the result of the two images
}
}
return tmp;
}
在這種特殊情況下,我假設操作符以tmp
的形式返回一個臨時變量,當你到達cpy = src + src
時,就會觸發移動分配操作符。 我不確定是否准確地說src + src
的結果是左值,因為事實上tmp
返回的是什么,但是tmp
被復制/分配給cpy
。 因此,在移動運算符存在之前,這將觸發默認的復制構造函數。 但是為什么在這種情況下不使用移動構造函數呢? 看來我還需要做一個:
Image cpy = std::move(src + src);
讓這個工作,我假設得到operator +
類Image的變量的xvalue?
有人可以幫助我更好地理解這個嗎? 告訴我什么不對勁?
謝謝。
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <fstream>
#include <cassert>
class Image
{
public:
Image() : w(512), h(512), d(NULL)
{
//printf("constructor default\n");
d = new float[w * h * 3];
memset(d, 0x0, sizeof(float) * w * h * 3);
}
Image(const unsigned int &_w, const unsigned int &_h) : w(_w), h(_h), d(NULL)
{
d = new float[w * h * 3];
memset(d, 0x0, sizeof(float) * w * h * 3);
}
// move constructor
Image(Image &&img) : w(0), h(0), d(NULL)
{
w = img.w;
h = img.h;
d = img.d;
img.d = NULL;
img.w = img.h = 0;
}
// move assignment operator
Image& operator = (Image &&img)
{
if (this != &img) {
if (d != NULL) delete [] d;
w = img.w, h = img.h;
d = img.d;
img.d = NULL;
img.w = img.h = 0;
}
return *this;
}
//~Image() { if (d != NULL) delete [] d; }
unsigned int w, h;
float *d;
};
int main(int argc, char **argv)
{
Image sample;// = readPPM("./lean.ppm");
Image res = sample;
return 0;
}
看來我還需要做一個:
Image cpy = std::move(src + src);
不是你的情況。 在
Image operator + (const Image &img) const {
Image tmp;
// ...
return tmp;
}
您正在創建並返回與函數的返回類型相同類型的對象。 這意味着return tmp;
將tmp
視為12.8 / 32(強調我的)的右值
當滿足或將滿足復制操作的省略標准時 ,除了源對象是函數參數這一事實, 並且要復制的對象由左值指定,重載決策選擇復制的構造函數是首先執行,好像對象是由右值指定的。
上述標准在12.8 / 31中給出,特別是第一個要點(強調我的):
- 在具有類返回類型的函數的return語句中,當表達式是 具有與函數返回類型相同的cv-unqualified類型 的非易失性自動對象 (除函數或catch子句參數之外)的名稱時,通過將自動對象直接構造到函數的返回值中, 可以省略復制/移動操作
實際上,仔細閱讀12.8 / 31表示,在您的情況下,允許編譯器(以及最受歡迎的編譯器)省略副本或完全移動。 這就是所謂的返回值優化 (RVO)。 確實,請考慮代碼的簡化版本:
#include <cstdlib>
#include <iostream>
struct Image {
Image() {
}
Image(const Image&) {
std::cout << "copy\n";
}
Image(Image&&) {
std::cout << "move\n";
}
Image operator +(const Image&) const {
Image tmp;
return tmp;
}
};
int main() {
Image src;
Image copy = src + src;
}
使用GCC 4.8.1編譯,此代碼不產生輸出,即不執行移動操作的副本。
讓代碼復雜化只是為了看看無法執行RVO時發生了什么。
Image operator +(const Image&) const {
Image tmp1, tmp2;
if (std::rand() % 2)
return tmp1;
return tmp2;
}
沒有太多細節,RVO不能在這里應用,不是因為標准禁止這樣做,而是出於其他技術原因。 通過operator +()
的這種實現,代碼輸出move
。 也就是說,沒有副本,只有移動操作。
最后一句話,基於Matthieu M對OP中zoska的回應。 正如Matthieu M正確地說的那樣,不建議return std::move(tmp);
因為它可以防止RVO。 的確,有了這個實現
Image operator +(const Image&) const {
Image tmp;
return std::move(tmp);
}
輸出是move
,也就是說,移動構造函數被調用,而正如我們所見, return tmp;
沒有調用復制/移動構造函數。 這是正確的行為,因為返回std::move(tmp)
的表達式不是上面引用的RVO規則所要求的非易失性自動對象的名稱。
更新響應user18490評論。 引入tmp
和tmp2
的operator +()
的實現是一種防止RVO的人為方式。 讓我們回到最初的實現,並考慮另一種阻止RVO的方法,它也顯示了完整的圖片:使用-fno-elide-constructors
選項編譯代碼(也可以在clang中使用)。 輸出(在GCC中但它可能在鏗鏘聲中有所不同)是
move
move
調用函數時,將分配堆棧內存以構建要返回的對象。 我強調這不是上面的變量tmp
。 這是另一個未命名的臨時對象。
然后, return tmp;
觸發復制或從tmp
移動到未命名的對象和初始化Image cpy = src + src;
最后將未命名的對象復制/移動到cpy
。 這是基本的語義。
關於第一次復制/移動,我們有以下內容。 由於tmp
是一個左值,因此復制構造函數通常用於從tmp
復制到未命名的對象。 但是,上面的特殊條款提出了一個例外,並說tmp
return tmp;
應該被認為是一個右值。 因此調用移動構造函數。 此外,當執行RVO時,移動被省略,並且實際上在未命名對象的頂部創建了tmp
。
關於第二次復制/移動它甚至更簡單。 未命名的對象是一個右值,因此選擇移動構造函數從它移動到cpy
。 現在,還有另一個優化(類似於RVO,但AFAIK沒有名稱)也在12.8 / 31(第三個要點)中說明,它允許編譯器避免使用未命名的臨時並使用cpy
的內存代替。 因此,當RVO和這種優化到位tmp
,未命名的對象和cpy
基本上都是“同一個對象”。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.