7

I'm really confused, I have looked this up a few times and it's still not clicking. What does the copy constructor and move constructor look like behind the scenes, in terms of memory usage? I really do not understand what "stealing resources" is either with the move constructor. Should the move constructor be used for dynamically allocated memory or memory on the stack?

Also I was told that if I have code like this:

void someFunction(Obj obj){
    //code
    cout << &obj << endl;
}

int main(){
    Obj o;
    cout << &o << endl;
    someFunction(o);
}

that o would be copied to obj. My impression was that copying creates a new item in memory and copies the passed data to that object's memory address. So obj would create a new space in memory and o's data would be copied to it. However I get the same exact address for o and obj, so basically I have no idea what's going on.

user13985180
  • 151
  • 1
  • 1
  • 9
  • 2
    `void someFunction(o);` is wrong, that won't compile. Don't show bogus code here, but give us a [mcve] as required please. See here: http://coliru.stacked-crooked.com/a/76629302616deccd – πάντα ῥεῖ Oct 15 '20 at 19:50
  • 1
    Read this: ["Move constructors" (from cppreference.com)](https://en.cppreference.com/w/cpp/language/move_constructor). – WhozCraig Oct 15 '20 at 19:50
  • Its simple enough, a copy constructor copies, the original is *unchanged*, a move constructor moves, the original is *not there any more* (loosely speaking). It has nothing at all to do with heap memory or stack memory or anything like that. – john Oct 15 '20 at 19:50
  • It's generally something like the difference between the work required to make a full copy vs. a few pointer re-directs. It's in the names. I use a pie analogy. Copy means I make a second pie from scratch and give it to you. Move means I slide my pie down the counter and give it to you. – sweenish Oct 15 '20 at 19:52
  • ^^^^ more accurately, "move" means he dumps his pie out of his pan and into your waiting, new, shiny empty pan. Now you have the goods and he has nada but a shell of his former self. No added baked goods required. – WhozCraig Oct 15 '20 at 19:54
  • 3
    Plus what you were told about that code is completely wrong (or you misunderstood what you were told) `obj` is a reference so it refers to the original object, which is why you see the same address. – john Oct 15 '20 at 19:56
  • Generally move constructors can be implemented more efficiently than copy constructors, so you use a move when you want efficiency and you don't mind that the original is destroyed. – john Oct 15 '20 at 19:59
  • The original object is not *destroyed* by the move, rather it is left in a *valid but unspecified state*. From a practical standpoint, that means it is suitable for destruction or to be assigned to which will put it into a *valid and specified state*. Some C++ standard library objects have additional guarantees, and your own member functions or the state of your own objects that are moved from may be valid-and-specified if you program them that way. – Eljay Oct 15 '20 at 20:31
  • 1
    @user13985180 Please stop editing the code in your question. You are invalidating your question. With your latest edit, "*I get the same exact address for `o` and `obj`*" no longer applies, but it did in your original example. – Remy Lebeau Oct 15 '20 at 21:16

2 Answers2

22

What does the copy constructor and move constructor look like behind the scenes, in terms of memory usage?

If any constructor is being called, it means a new object is being created in memory. So, the only difference between a copy constructor and a move constructor is whether the source object that is passed to the constructor will have its member fields copied or moved into the new object.

I really do not understand what "stealing resources" is either with the move constructor.

Imagine an object containing a member pointer to some data that is elsewhere in memory. For example, a std::string pointing at dynamically allocated character data. Or a std::vector pointing at a dynamically allocated array. Or a std::unique_ptr pointing at another object.

A copy constructor must leave the source object intact, so it must allocate its own copy of the object's data for itself. Both objects now refer to different copies of the same data in different areas of memory (for purposes of this beginning discussion, lets not think about reference-counted data, like with std::shared_ptr).

A move constructor, on the other hand, can simply "move" the data by taking ownership of the pointer that refers to the data, leaving the data itself where it resides. The new object now points at the original data, and the source object is modified to no longer point at the data. The data itself is left untouched.

That is what makes move semantics more efficient than copy/value sematics.

Here is an example to demonstrate this:

class MyIntArray
{
private:
    int *arr = nullptr;
    int size = 0;

public:
    MyIntArray() = default;

    MyIntArray(int size) {
        arr = new int[size];
        this->size = size;
        for(int i = 0; i < size; ++i) {
            arr[i] = i;
        }
    }

    // copy constructor
    MyIntArray(const MyIntArray &src) {
        // allocate a new copy of the array...
        arr = new int[src.size];
        size = src.size;
        for(int i = 0; i < src.size; ++i) {
            arr[i] = src.arr[i];
        }
    }

    // move constructor
    MyIntArray(MyIntArray &&src) {
        // just swap the array pointers...
        src.swap(*this);
    }

    ~MyIntArray() {
        delete[] arr;
    }

    // copy assignment operator
    MyIntArray& operator=(const MyIntArray &rhs) {
        if (&rhs != this) {
            MyIntArray temp(rhs); // copies the array
            temp.swap(*this);
        }
        return *this;
    }

    // move assignment operator
    MyIntArray& operator=(MyIntArray &&rhs) {
        MyIntArray temp(std::move(rhs)); // moves the array
        temp.swap(*this);
        return *this;
    }

    /*
    or, the above 2 operators can be implemented as 1 operator, like below.
    This allows the caller to decide whether to construct the rhs parameter
    using its copy constructor or move constructor...

    MyIntArray& operator=(MyIntArray rhs) {
        rhs.swap(*this);
        return *this;
    }
    */

    void swap(MyIntArray &other) {
        // swap the array pointers...
        std::swap(arr, other.arr);
        std::swap(size, other.size);
    }
};
void copyArray(const MyIntArray &src)
{
    MyIntArray arr(src); // copies the array
    // use arr as needed...
}

void moveArray(MyIntArray &&src)
{
    MyIntArray arr(std::move(src)); // moved the array
    // use arr as needed...
}

MyIntArray arr1(5);                // creates a new array
MyIntArray arr2(arr1);             // copies the array
MyIntArray arr3(std::move(arr2));  // moves the array
MyIntArray arr4;                   // default construction
arr4 = arr3;                       // copies the array
arr4 = std::move(arr3);            // moves the array
arr4 = MyIntArray(1);              // creates a new array and moves it

copyArray(arr4);                   // copies the array
moveArray(std::move(arr4));        // moves the array

copyArray(MyIntArray(10));         // creates a new array and copies it
moveArray(MyIntArray(10));         // creates a new array and moves it

Should the move constructor be used for dynamically allocated memory or memory on the stack?

Move semantics are most commonly used with pointers/handles to dynamic resources, yes (but there are other scenarios where move semantics can be useful). Updating pointers to data is faster than making copies of data. Knowing that the source object will not need to refer to its data anymore, there is no need to copy the data and then destroy the original, the original can be "moved" as-is from the source object to the destination object.

Where move semantics doesn't help improve efficiency is when the data being "moved" is POD data (plain old data, ie integers, floating-point decimals, booleans, struct/array aggregates, etc). "Moving" such data is the same as "copying" it. For instance, you can't "move" an int to another int, you can only copy its value.

Also I was told that if I have code like this: ... that o would be copied to obj.

In the example of someFunction(Obj obj), yes, since it takes its obj parameter by value, thus invoking the copy constructor of Obj to create the obj instance from o.

Not in the example of someFunction(Obj &&obj) or someFunction(const Obj &obj), no, since they take their obj parameter by reference instead, thus there is no new object created at all. A reference is just an alias to an existing object (behind the scenes, it is implemented as a pointer to the object). Applying the & address-of operator to a reference will return the address of the object that is being referred to. Which is why you see the same address being printed in main() and someFunction() in those examples.

My impression was that copying creates a new item in memory and copies the passed data to that object's memory address.

Essentially, yes. It would be more accurate to say that it copies the values of the passed object's member fields to the corresponding member fields of the new object.

So obj would create a new space in memory and o's data would be copied to it.

Only if obj is a copy of o, yes.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • Great answer. It's worth linking to the [What is move semantics?](https://stackoverflow.com/questions/3106110/what-is-move-semantics) question. You could also mention when one cannot benefit from move semantics. One of the answers to the above question discusses this. – darcamo Oct 15 '20 at 21:00
  • @darcamo added now – Remy Lebeau Oct 15 '20 at 21:12
  • Hi, 3 years later I have a simple question. It seems to me that std::move() is the only way to call the move constructor or the move assignment operator, am I right? – puccj Apr 27 '23 at 10:36
  • 1
    @puccj no, the move constructor and move assignment operator can be called with any rvalue as input, ie temporary objects, etc. `std::move()` simply casts an lvalue (ie, an existing variable) into an rvalue so it can be moved from. – Remy Lebeau Apr 27 '23 at 17:07
1

Remy's answer is very good and there are other related questions here that cover this. But if the answers still look somewhat "abstract" to you, then the best way is to see what really happens by yourself. Consider the class below.

void print_vec(auto v) {
    std::cout << "[";
    for(auto elem : v) {
        std::cout << elem << ", ";
    }
    std::cout << "]" << std::endl;
}

class Myclass {
public:
    // Public data to make inspection easy in this example
    int a;
    std::vector<int> v;

    Myclass(int pa, std::vector<int> pv) : a(pa), v(std::move(pv)) {}

    void print(std::string_view label) {
        std::cout << label << " object\n    a stored in " << &a << " with value " << a << "\n";
        std::cout << "    v elements stored in " << v.data() << " with value ";
        print_vec(v);
        std::cout << std::endl;
    }
};

int main(int argc, char *argv[])
{
    Myclass obj1(10, {1,2,3,4,5});

    std::cout << "xxxxxxxxxx Part 1 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" << std::endl;
    obj1.print("obj1");

    std::cout << "xxxxxxxxxx Part 2 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" << std::endl;
    // Now let's create a copy -> This calls the copy constructor
    auto obj2 = obj1;

    obj1.print("obj1");
    obj2.print("obj2");

    std::cout << "xxxxxxxxxx Part 3 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" << std::endl;
    // Now let's call the move constructor
    auto obj3 = std::move(obj1);

    obj1.print("obj1");
    obj3.print("obj3");

    return 0;
}

Myclass has to data members. One integer and a std::vector. If you run this code you get something like below

xxxxxxxxxx Part 1 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
obj1 object
    'a' stored in 0x7ffd1c8de0f0 with value 10
    'v' elements stored in 0x55946fab8eb0 with value [1, 2, 3, 4, 5, ]

xxxxxxxxxx Part 2 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
obj1 object
    'a' stored in 0x7ffd1c8de0f0 with value 10
    'v' elements stored in 0x55946fab8eb0 with value [1, 2, 3, 4, 5, ]

obj2 object
    'a' stored in 0x7ffd1c8de110 with value 10
    'v' elements stored in 0x55946fab92e0 with value [1, 2, 3, 4, 5, ]

xxxxxxxxxx Part 3 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
obj1 object
    'a' stored in 0x7ffd1c8de0f0 with value 10
    'v' elements stored in 0 with value []

obj3 object
    'a' stored in 0x7ffd1c8de130 with value 10
    'v' elements stored in 0x55946fab8eb0 with value [1, 2, 3, 4, 5, ]

Part 1

Here we are just printing the original object.

Part 2

Now we are creating a second object by copying the first one. This calls the copy constructor of Myclass. The data in both objects is the same as expected, but they are copies, since the memory address is different in both objects and ``.

Part 3

We create yet another object, but now we move obj1 to this new object. This calls the move constructor of our class. Once one object is moved from, as obj1 was, we should not use it again unless we assign a new value to it. Now the internal pointer in obj1.v is a nullpointer and notice that the address where obj3.v stores its own data points to where obj1.v was pointing to before. That's what means to move data from one object to another.

But not everything can be or is moved. Notice that the integer a member was copied, while the vector v member was moved. While an int is cheap to copy, sometimes even data that is not cheap to copy can't be moved. For instance, if we add a double arr[100] data to Myclass then the move constructor would still copy obj1.arr to the new obj3 object, since arr is in the stack.

darcamo
  • 3,294
  • 1
  • 16
  • 27