不确定理解移动构造函数的优势(或者它是如何工作或使用的)

use*_*490 7 c++ move-semantics c++11

我最近在SE上发布了一个关于下面代码的问题,因为它产生了编译错误.当你实现移动构造函数或移动赋值运算符时,有人会回答这个问题,然后删除默认的复制构造函数.他们还建议我然后使用它std::move()来获得这样的东西:

Image src(200, 200);
Image cpy = std::move(src);
Run Code Online (Sandbox Code Playgroud)

现在这对我有意义,因为在这种情况下你想要使用移动赋值运算符或移动构造函数的事实必须明确.src在这个例子中是一个左值,没有任何东西可以告诉编译器,而不是你实际想要移动它的内容,cpy除非你明确表达std::move.但是,我对此代码有更多问题:

Image cpy = src + src
Run Code Online (Sandbox Code Playgroud)

我没有把副本放在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; 
}
Run Code Online (Sandbox Code Playgroud)

在这种特殊情况下,我假设操作符以形式返回临时变量,tmp并且当你到达时,将触发移动分配操作符cpy = src + src.我不确定结果src + src是左值是否准确,因为实际上是什么返回tmp,但随后tmp被复制/分配cpy.因此,在移动运算符存在之前,这将触发默认的复制构造函数.但是为什么在这种情况下不使用移动构造函数呢?看来我还需要做一个:

Image cpy = std::move(src + src);
Run Code Online (Sandbox Code Playgroud)

为了让这个工作,我假设得到一个由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;
}
Run Code Online (Sandbox Code Playgroud)

Cas*_*eri 6

看来我还需要做一个:

Image cpy = std::move(src + src);
Run Code Online (Sandbox Code Playgroud)

不是你的情况.在

Image operator + (const Image &img) const {
    Image tmp;
    // ...
    return tmp; 
}
Run Code Online (Sandbox Code Playgroud)

您正在创建并返回与函数的返回类型相同类型的对象.这暗示return tmp;tmp根据12.8/32(强调我的)考虑好像它是一个rvalue

当满足或将满足复制操作的省略标准时,除了源对象是函数参数这一事实,并且要复制的对象由左值指定,重载决策选择复制的构造函数是首先执行,好像对象是由右值指定的.

上述标准在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;
}
Run Code Online (Sandbox Code Playgroud)

使用GCC 4.8.1编译,此代码不产生输出,即不执行移动操作的副本.

让代码复杂化只是为了看看无法执行RVO时发生了什么.

    Image operator +(const Image&) const {
        Image tmp1, tmp2;
        if (std::rand() % 2)
            return tmp1;
        return tmp2;
    }
Run Code Online (Sandbox Code Playgroud)

没有太多细节,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);
    }
Run Code Online (Sandbox Code Playgroud)

输出是move,即移动构造函数被调用,而正如我们所见,return tmp;没有复制/移动构造函数被调用.这是正确的行为,因为表达式是回报std::move(tmp)不是非易失性自动对象的所要求的上面引用的RVO规则的名称.

更新响应user18490评论.其实施operator +()介绍tmp并且tmp2是防止RVO的人为方式.让我们回到最初的实现,并考虑另一种防止RVO的方法,它也显示了完整的图片:使用选项编译代码-fno-elide-constructors(也可以在clang中使用).输出(在GCC中但它可能在铿锵声中有所不同)是

move
move
Run Code Online (Sandbox Code Playgroud)

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

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

关于第一次复制/移动,我们有以下内容.由于tmp是左值,因此通常使用复制构造函数将其复制tmp到未命名的对象.然而,在特别条款上面做一个例外,说tmpreturn tmp;应该考虑,如果它是一个右值.因此调用移动构造函数.此外,执行RVO时,移动被省略,tmp并且实际上是在未命名对象的顶部创建的.

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