[英]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.