Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Not sure to understand the advantage of the move constructor (or how it works or use it)

I recently posted a question on SE regarding the code below, because it generated a compilation error. Someone was kind enough to answer that when you implement a move constructor or move assignment operator then the default copy constructor is deleted. They also suggested that then I needed to use the std::move() to get something like this to work:

Image src(200, 200);
Image cpy = std::move(src);

Now that sort of makes sense to me, because the fact that you want to use the move assignment operator or move constructor in this case has to made explicit. src in this example is an lvalue and nothing can tell the compiler than you actually want to move its content to cpy unless you express this explicitly with std::move. However, I have more of a problem with this code:

Image cpy = src + src

I didn't put the copy for the operator + below but it's a straightforward overload operator of the type:

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; 
}

In this particular case, I would assume the operator returns a temp variable in the form of tmp and that the move assignement operator would be triggered in that case when you get to cpy = src + src. I am not sure it's accurate to say that the result of src + src is a lvalue because in fact what's returns in tmp, but then tmp is copied/assigned to cpy. So before the move operator existed, this would have triggered the default copy constructor. But why isn't it using the move constructor in this case? It seems that I also need to do a:

Image cpy = std::move(src + src);

to get this to work, which I assume gets an xvalue for the variable returned by the operator + of the class Image?

Could someone helps me understanding this better please? and tell what I don't get right?

Thank you.

#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;
}
like image 310
user18490 Avatar asked Oct 18 '13 08:10

user18490


1 Answers

It seems that I also need to do a:

Image cpy = std::move(src + src);

Not in your case. In

Image operator + (const Image &img) const {
    Image tmp;
    // ...
    return tmp; 
}

You are creating and returning an object of the same type as the return type of the function. This implies that return tmp; will consider tmp as if it was an rvalue as per 12.8/32 (emphasis mine)

When the criteria for elision of a copy operation are met or would be met save for the fact that the source object is a function parameter, and the object to be copied is designated by an lvalue, overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue.

The mentioned criteria are given in 12.8/31, in particular, the first bullet point says (emphasis mine):

in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cv-unqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value

Actually, a carefull reading of 12.8/31 says that in your case compilers are allowed (and the most popular ones do) to omit the copy or move altogether. This is the so called return value optimization (RVO). Indeed, consider this simplified version of your code:

#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;
}

Compiled with GCC 4.8.1, this code produces no output, that is, no copy of move operation is performed.

Let's complicate the code a little bit just to see what happened when RVO cannot be performed.

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

Without much of details, RVO cannot be applied here not because the standard forbids so but for other technical reasons. With this implementation of operator +() the code outputs move. That is, there's no copy, only a move operation.

A last word, based Matthieu M's response to zoska in the OP. As Matthieu M rightly said, it's not advisable to do return std::move(tmp); because it prevents RVO. Indeed, with this implementation

    Image operator +(const Image&) const {
        Image tmp;
        return std::move(tmp);
    }

The output is move, that is, the move constructor is called, whereas, as we've seen, with return tmp; no copy/move constructor is called. That's the correct behaviour because the expression being return std::move(tmp) is not the name of a non-volatile automatic object as required by the RVO rule quoted above.

Update In response to user18490 comment. The implementation of operator +() which introduces tmp and tmp2 is rather an artificial way to prevent RVO. Let's go back to the initial implementation and consider another way of preventing RVO which also shows the complete picture: compile the code with the option -fno-elide-constructors (also available in clang). The output (in GCC but it might vary in clang) is

move
move

When a function is called stack memory is allocated to build the object to be returned. I emphasize that this is not the variable tmp above. This another unnamed temporary object.

Then, return tmp; triggers a copy or move from tmp to the unnamed object and the initialization Image cpy = src + src; finally copy/move the unnamed object into cpy. That's the basic semantics.

Regarding the first copy/move we have the following. Since tmp is an lvalue the copy constructor would normally be used to copy from tmp to the unnamed object. However, the special clause above makes an exception and says that tmp in return tmp; should be considered as if it was an rvalue. Hence the move constructor is called. In addition, when RVO is performed, the move is elided and tmp is actually created on top of the unnamed object.

Regarding the second copy/move it's even simpler. The unnamed object is an rvalue and therefore the move constructor is selected to move from it to cpy. Now, there's another optimization (which is similar to RVO but AFAIK doesn't have a name) also stated in 12.8/31 (third bullet point) that allows the compiler to avoid the use of the unnamed temporary and use the memory of cpy instead. Therefore, when RVO and this optimization are in place tmp, the unnamed object and cpy are essentially "the same object".

like image 156
Cassio Neri Avatar answered Sep 28 '22 07:09

Cassio Neri