Hi
It's been a while i'm learing C++ but still i have some issue with Move semantics and some other stuff.
I wrote this class but i don't know why the Copy constructor is getting called
and the output is a little strange to me, shouldn't the Move Constructor gets called three times only? i don't know what i did wrong.

Also in the Move Connstructor i could use Data(other.Data) but in the Copy Constructor i couldn't
i had to write a for loop or use std::copy, why?

I appreciate your answers.

Output

Move constructor is called!
Move constructor is called!
Copy constructor is called!
Move constructor is called!
Copy constructor is called!
Copy constructor is called!

main.cpp

#include <iostream>
#include <string>
#include <vector>
#include <utility>
#include <algorithm>

class Foo
{
private:
    std::size_t size;
    int* Data;

public:
    Foo(std::size_t length): size(length), Data(new int [length])
    {}

    //Copy Constructor
    Foo(const Foo& other): size(other.size), Data(new int[other.size])
    {
        std::cout<<"Copy constructor is called!"<<std::endl;

        std::copy(other.Data, other.Data + size, Data);
    }

    //Move Constructor
    Foo(Foo&& other): size(other.size), Data(other.Data)
    {
        std::cout<<"Move constructor is called!"<<std::endl;

       other.size=0;
       other.Data = nullptr;
    }

    std::size_t retSize() const
    {
        return size;
    }

    ~Foo()
    {
        delete [] Data;
        size = 0;
    }
};

int main ()
{
    std::vector <Foo> vec;
    vec.push_back(Foo(15));
    vec.push_back(Foo(12));
    vec.push_back(Foo(17));

    return 0;
}

Recommended Answers

All 3 Replies

This is a bit tricky to explain, because it involves a number of implementation details about std::vector. It is clearer if we expand the main code a bit, with some additional print-outs:

std::vector <Foo> vec;
std::cout << " Adding..." << std::endl;
vec.push_back(Foo(15));
std::cout << " Adding..." << std::endl;
vec.push_back(Foo(12));
std::cout << " Adding..." << std::endl;
vec.push_back(Foo(17));
std::cout << " Adding..." << std::endl;
vec.push_back(Foo(17));
std::cout << " Adding..." << std::endl;
vec.push_back(Foo(17));
std::cout << " Adding..." << std::endl;
vec.push_back(Foo(17));
std::cout << " Done!" << std::endl;

Which gives the following output:

 Adding...
Move constructor is called!
 Adding...
Move constructor is called!
Copy constructor is called!
 Adding...
Move constructor is called!
Copy constructor is called!
Copy constructor is called!
 Adding...
Move constructor is called!
 Adding...
Move constructor is called!
Copy constructor is called!
Copy constructor is called!
Copy constructor is called!
Copy constructor is called!
 Adding...
Move constructor is called!
 Done!

So, the first thing to observe is that for each push-back, there is a call to the move-constructor. This move-constructor is used to put the new element given (which is a temporary "rvalue" object given to the push-back function) into its place in the dynamic memory maintained by the std::vector container. So, the idea there is that if you give the vector an rvalue, it's going to do the most optimal thing, which is to move it into where it needs to go in the container's memory.

The second thing you'll notice is the uneven number of moves and copies for each subsequent push-back calls. This is related to the geometric growth of the std::vector. It's explained nicely on the wikipedia page on dynamic arrays. The idea is that instead of reallocating every time the array grows in size, you would instead preallocate a larger capacity (usually as a multiple of the existing capacity, and hence, "geometrically" expanding) whenever you exhaust your current capacity. This leads to much less frequent reallocations, and thus, a better performance overall.

So, in the above output, when you see only a single move-constructor call for a given push-back call, it means that no reallocation was needed, but when you see the move-constructor call followed by several copy-constructors calls, it means that a reallocation was needed (move the new element, and copy all the old elements to the newly allocated memory).

The frequency of those reallocations depend on the implementation details of the std::vector and the growth factor it uses (often-cited growth factor is 2, but many practical implementations use something more between 1.25 (5/4) and 1.67 (5/3)).

Finally, you might wonder why the vector would not move all the old elements into the newly allocated memory, since it would be more efficient. The reason for this is exception-safety guarantees. The standard requires that push-back provides some pretty strong exception-safety guarantees. If one where to use the move-constructor to move the existing elements from the old memory to the new memory, and an exception got thrown in the middle of that operation, it would be impossible to "revert" back to the original state of the container, because half of the elements would be in the old memory and half would be in the new memory, and there would be no safe (non-throwing) way to bring them back.

So, the rule is that if there is a non-throwing move-constructor available (i.e., you mark your move-constructor as noexcept), then the vector container is allowed to use it to safely move existing elements from the old memory into the new. If not, the elements must be copied (because even if a copy throws an exception, the original elements in their original memory are still available and can be reverted back to). This way, push-back is pretty much guaranteed to be in its original state (before the push-back call) if an exception occurs.

So, if you mark your move-constructor as noexcept, as so:

//Move Constructor
Foo(Foo&& other) noexcept : size(other.size), Data(other.Data)
{
    std::cout << "Move constructor is called!" << std::endl;
    other.size=0;
    other.Data = nullptr;
}

Then, you get the following output:

 Adding...
Move constructor is called!
 Adding...
Move constructor is called!
Move constructor is called!
 Adding...
Move constructor is called!
Move constructor is called!
Move constructor is called!
 Adding...
Move constructor is called!
 Adding...
Move constructor is called!
Move constructor is called!
Move constructor is called!
Move constructor is called!
Move constructor is called!
 Adding...
Move constructor is called!
 Done!

Which is clearly way more efficient than the previous version. So, the lesson here is that if you can implement a non-throwing move-constructor, then be sure to mark it as noexcept.

Also in the Move Connstructor i could use Data(other.Data) but in the Copy Constructor i couldn't
i had to write a for loop or use std::copy, why?

Because the copy-constructor must perform a deep-copy, while the move-constructor simply hands over ownership of the data from the "other" object to the newly created one. Please read my tutorial for more explanation of this..

commented: Well explained +8

If you know how many elements you're going to add to the vector (or you at least have a reasonable upper bound), you can also use reserve to reserve the proper amount of memory up front, preventing reallocations. Then you'd have exactly one move constructor call per push_back and no calls of the copy constructor.

Thanks Mike, very well explained.
I'll make sure to read your tutorials.

Thanks sepp2k.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.