7

我最近在 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);

为了让它工作,我假设它为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;
}
4

1 回答 1

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 语句中,当表达式是具有与函数返回类型相同的 cv 非限定类型的非易失性自动对象(函数或 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 评论。其实施operator +()引入了tmp并且tmp2是一种防止RVO的人为方式。让我们回到最初的实现,考虑另一种防止 RVO 的方法,它也显示了完整的画面:使用选项编译代码-fno-elide-constructors(也可以在 clang 中获得)。输出(在 GCC 中,但在 clang 中可能会有所不同)是

move
move

当调用函数时,分配堆栈内存来构建要返回的对象。我强调这不是tmp上面的变量。这是另一个未命名的临时对象。

然后,return tmp;触发复制或移动tmp到未命名对象,初始化Image cpy = src + src;最终将未命名对象复制/移动到cpy. 这就是基本语义。

关于第一次复制/移动,我们有以下内容。由于tmp是左值,因此复制构造函数通常用于复制tmp到未命名对象。但是,上面的特殊条款有一个例外,它说tmpinreturn tmp;应该被认为是一个右值。因此调用了移动构造函数。此外,当执行 RVO 时,移动被省略,tmp实际上是在未命名的对象之上创建的。

关于第二次复制/移动它更简单。未命名的对象是一个右值,因此选择移动构造函数从它移动到cpy. 现在,还有另一个优化(类似于 RVO,但 AFAIK 没有名称)也在 12.8/31(第三个要点)中说明,它允许编译器避免使用未命名的临时并使用内存来cpy代替。因此,当 RVO 和这个优化到位tmp时,未命名的对象和cpy本质上是“同一个对象”。

于 2013-10-18T08:54:22.580 回答