我最近在SE上发布了一个关于下面代码的问题,因为它产生了编译错误。当你实现移动构造函数或移动赋值运算符时,有人会回答这个问题,然后删除默认的复制构造函数。他们还建议我然后需要使用std::move()
来实现这样的工作:
Image src(200, 200);
Image cpy = std::move(src);
现在这对我有意义,因为在这种情况下你想要使用移动赋值运算符或移动构造函数的事实必须明确。此示例中的src
是左值,除非您使用cpy
明确表达,否则没有任何内容可以告诉编译器您实际想要将其内容移动到std::move
。但是,我对此代码有更多问题:
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);
让这个工作,我假设得到类Image的operator +
返回的变量的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;
}
答案 0 :(得分:6)
似乎我还需要做一个:
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语句中,当表达式是非易失性自动对象的名称时(函数或catch子句参数除外)使用与函数返回类型相同的cv-unqualified类型,通过将自动对象直接构造为函数的返回值
,可以省略复制/移动操作
实际上,仔细阅读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规则所要求的非易失性自动对象的名称。
更新响应用户18490评论。引入operator +()
和tmp
的{{1}}的实施是一种预防RVO的人为方式。让我们回到最初的实现,并考虑另一种阻止RVO的方法,它也显示了完整的图片:使用选项tmp2
编译代码(也可以在clang中使用)。输出(在GCC中但可能在clang中有所不同)是
-fno-elide-constructors
调用函数时,会分配堆栈内存来构建要返回的对象。我强调这不是上面的变量move
move
。这是另一个未命名的临时对象。
然后,tmp
触发副本或从return tmp;
移动到未命名对象,初始化tmp
最终将未命名对象复制/移动到Image cpy = src + src;
。这是基本的语义。
关于第一次复制/移动,我们有以下内容。由于cpy
是左值,因此通常使用复制构造函数从tmp
复制到未命名的对象。但是,上面的特殊条款提出了一个例外,并说tmp
中的tmp
应该被视为rvalue。因此调用移动构造函数。此外,当执行RVO时,移动被省略,并且实际上在未命名对象之上创建了return tmp;
。
关于第二次复制/移动它甚至更简单。未命名的对象是一个右值,因此选择移动构造函数从它移动到tmp
。现在,还有另一个优化(类似于RVO,但AFAIK没有名称)也在12.8 / 31(第三个要点)中说明,它允许编译器避免使用未命名的临时文件并使用{{{ 1}}而不是。因此,当RVO和此优化到位cpy
时,未命名对象和cpy
基本上是“同一个对象”。