Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is this why the move constructor in C++ 11 makes sense?

Tags:

c++

c++11

I was at a lecture with Bjarne Stoustrup recently, he was talking about c++ 11 and why it made sense.

One of his examples of new awesomeness was the news '&&' symbol for move constructors.

Then I want home and started thinking, "When would I ever need such a thing?".

My first example was the code below:

class Number {
private:
    int value;
public:
    Number(const int value) : value(value){
        cout << "Build Constructor on " << value << endl;
    }
    Number(const Number& orig) : value(orig.value){
        cout << "Copy Constructor on " << value << endl;        
    }
    virtual ~Number(){}

    int toInt() const{
        return value;
    }


    friend const Number operator+(const Number& n0, const Number& n1); 
};

const Number operator+(const Number& n0, const Number& n1){
    return  Number(n0.value + n1.value);
}

int main(int argc, char** argv) {

    const Number n3 = (Number(2) + Number(1));
    cout << n3.toInt() << endl;
    return 0;
}

This code does exactly what the move constructor is supposed to solve. The n3 variable is constructed from a reference to the value returned from the '+' operator.

Except this is the output from running the code:

Build Constructor on 1
Build Constructor on 2
Build Constructor on 3
3

RUN SUCCESSFUL 

What the output shows is that the copy constructor never gets called -- and this is with optimizations turned off. I'm having a hard time twisting the arm of the code enough to make it run the copy construtor. wrapping the result in a std::pair did the trick, but it kept me thinking.

Is the argument of move-constructors in operator arithmetic's actually a failed argument?

Why is'nt my copy constructor called and why is it called in :

using namespace std;

class Number {
private:
    int value;
public:
    Number(const int value) : value(value){
        cout << "Build Constructor on " << value << endl;
    }
    Number(const Number& orig) : value(orig.value){
        cout << "Copy Constructor on " << value << endl;        
    }
    virtual ~Number(){}

    int toInt() const{
        return value;
    }


    friend const std::pair<const Number, const Number> operator+(const Number& n0, const Number& n1); 
};

const std::pair<const Number, const Number> operator+(const Number& n0, const Number& n1){
    return  make_pair(Number(n0.value + n1.value), n0);
}

int main(int argc, char** argv) {

    const Number n3 = (Number(2) + Number(1)).first;
    cout << n3.toInt() << endl;
    return 0;
}

With output:

Build Constructor on 1
Build Constructor on 2
Copy Constructor on 2
Build Constructor on 3
Copy Constructor on 3
Copy Constructor on 2
Copy Constructor on 3
Copy Constructor on 2
Copy Constructor on 3
3

RUN SUCCESSFUL 

I would like to know what the logic is and why the pair operator basically screws up the performance?

update:

I did another modification and found that if I replaced make_pair with the actual templated constructor of the pair pair<const Number, const Number> this reduced the number of times the copy constructor got fired:

class Number {
private:
    int value;
public:
    Number(const int value) : value(value){
        cout << "Build Constructor on " << value << endl;
    }
    Number(const Number& orig) : value(orig.value){
        cout << "Copy Constructor on " << value << endl;        
    }
    virtual ~Number(){}

    int toInt() const{
        return value;
    }


    friend const std::pair<const Number, const Number> operator+(const Number& n0, const Number& n1); 
};



const std::pair<const Number, const Number> operator+(const Number& n0, const Number& n1){
    return  std::pair<const Number, const Number>(Number(n0.value + n1.value), n0);
}

int main(int argc, char** argv) {

    const Number n3 = (Number(2) + Number(1)).first;
    cout << n3.toInt() << endl;
    return 0;
}

output :

Build Constructor on 1
Build Constructor on 2
Build Constructor on 3
Copy Constructor on 3
Copy Constructor on 2
Copy Constructor on 3
3

RUN SUCCESSFUL

So it would apear using make_pair is harmfull?

like image 357
Martin Kristiansen Avatar asked May 20 '12 10:05

Martin Kristiansen


People also ask

Why do we need move constructor?

A move constructor enables the resources owned by an rvalue object to be moved into an lvalue without copying. For more information about move semantics, see Rvalue Reference Declarator: &&. This topic builds upon the following C++ class, MemoryBlock , which manages a memory buffer.

What is move in C++ 11?

C& C::operator=(C&& other);//C++11 move assignment operator. A move assignment operator is similar to a copy constructor except that before pilfering the source object, it releases any resources that its object may own.

What does move () do?

Move Constructor And Semantics: std::move() is a function used to convert an lvalue reference into the rvalue reference. Used to move the resources from a source object i.e. for efficient transfer of resources from one object to another. std::move() is defined in the <utility> header.

What is advantage of move constructor in C++?

Move constructor moves the resources in the heap, i.e., unlike copy constructors which copy the data of the existing object and assigning it to the new object move constructor just makes the pointer of the declared object to point to the data of temporary object and nulls out the pointer of the temporary objects.


2 Answers

Consider this simple C++ code:

class StringHolder
{
  std::string member;
public:

  StringHolder(const std::string &newMember) : member(newMember) {}
};

std::string value = "I am a string that will probably be heap-allocated.";
StringHolder hold(value);

After executing the second line, how many copies of the string exist? The answer is two: one stored in value, and one stored in hold. That is fine... sometimes. There will often be times when you want to give someone a copy of a string while keeping it for yourself. But there are times you don't want to do that too. For example:

StringHolder hold("I am a string that will probably be heap-allocated.");

This will create a std::string temporary, which will then be passed to StringHolder's constructor. The constructor will copy-construct its member. After the constructor completes, the temporary will be destroyed. At one point, we had two copies of the string, for no reason whatsoever.

There was no point of having two copies of the string. What we wanted to do was move the std::string parameter into the StringHolder, so that there's only ever one copy of the string.

That's where move construction comes in.

A std::string is basically just a wrapper around a pointer to an allocated array of characters, and a size containing the length of that array (and a capacity, but nevermind that now). If you have a std::string, and you want to move it into the other, then the new string must claim ownership of that allocated array of characters, and the old string must relinquish ownership. In C++03, you could do that with a swap operation:

std::string oldStr = "I am a string that will probably be heap-allocated.";
std::string newStr;
std::swap(newStr, oldStr);

This moves the contents of oldStr into newStr without any memory allocation.

C++11's move syntax provides two important features that std::swap does not.

First, move can happen implicitly (but only when it's safe to do so). You must explicitly call swap if you want swapping; moving can happen by writing natural code. For example, take our StringHolder from before and make one change:

class StringHolder
{
  std::string member;
public:

  StringHolder(std::string newMember) : member(std::move(newMember)) {}
};

StringHolder hold("I am a string that will probably be heap-allocated.");

How many copies of this string are ever created? The answer is... just one: the construction of the temporary. Because it is a temporary, C++11 is smart enough to know that it can move-construct anything being initialized by it. So it move-constructs the value parameter of the StringHolder constructor (or more likely elides the construction altogether). This moves the stored memory from the temporary into newMember. So no copying takes place.

After that, we invoke the move constructor explicitly when we construct member. This again moves the allocated memory from newMember to member.

We only ever allocate a string once. That can be a big savings in performance.

Now, how does this relate to constructors of your own types? Well, consider this code:

class StringHolder
{
  std::string member;
public:

  StringHolder(std::string newMember) : member(std::move(newMember)) {}

  StringHolder(const StringHolder &old) : member(old.member) {}
  StringHolder(StringHolder &&old) : member(std::move(old.member)) {}
};

StringHolder oldHold = std::string("I am a string that will probably be heap-allocated.");
StringHolder newHold(oldHold);

This time, we now have a class with a copy and move constructor. How many copies of the string do we get?

Two. Of course it's two. We have oldHold and newHold, each with a copy of the string.

But, if we did this:

StringHolder oldHold = std::string("I am a string that will probably be heap-allocated.");
StringHolder newHold(std::move(oldHold));

Then there would again only ever be one copy of the string lying around.

That's why movement is important. That's why it matters: it reduces the number of copies of things you may need to have lying around.


Why is'nt my copy constructor called

Your copy constructor wasn't called because it was elided. It's doing return-value optimization. Turning off optimization isn't going to help, because most compilers will elide anyway. There is no reason not to when elision is possible.

For function return values, movement is important in cases where elision is not possible.

like image 200
Nicol Bolas Avatar answered Nov 15 '22 17:11

Nicol Bolas


It might help to understand the value of move semantics if you overload your operator+ with this one:

Number operator+(Number&& n0, Number&& n1){
  n0.value += n1.value;
  return std::move(n0);
}

This has two important changes:

  • it returns a non-const value
  • it accepts rvalue arguments and modifies one of them instead of creating a new object

This allows your example to avoid one of the "Build Constructor" calls

Build Constructor on 1
Build Constructor on 2
Copy Constructor on 3
3

Now the code creates two new objects and copies one, nstead of creating three new ones, so not a big advantage so far. But if you add a move constructor that can be used instead of the copy:

Number(Number&& orig) : value(orig.value){
  cout << "Move Constructor on " << value << endl;
}

Build Constructor on 1
Build Constructor on 2
Move Constructor on 3
3

If the class allocates memory in the "Build" and "Copy" constructors you have reduced the total number of allocations from three in your original code to two (assuming the move constructor doesn't allocate anything, but takes ownership of the memory that was owned by the object it moves from.)

Now if you change the calculation to:

Number n3 = Number(2) + Number(1) + Number(0);

And compare your original code with the move-enabled version you should see the number of "allocations" reduced from five to three. The more temporaries that are involved the greater the benefit of modifying and moving from temporaries instead of creating new objects. The benefit is not only avoiding copies, but also avoiding creating new resources for new objects, by taking ownership of the resources from existing objects instead.

like image 31
Jonathan Wakely Avatar answered Nov 15 '22 18:11

Jonathan Wakely