Kris
Kris

Reputation: 10307

Copy Constructors and Move Semantics by Following Objects

I've read countless articles on copy constructors and move semantics. I feel like I 'sort' of understand what's going on, but a lot of the explanations leave out whats actually occurring under the hood (which is what is causing me confusion).

For example:

string b(x + y);

string(string&& that)
    {
        data = that.data;
        that.data = 0;
    }

What is actually happening in memory with the objects? So you have some object 'b' that takes x + y which is an rvalue and then that invokes the move constructor. This is really causing me confusion... Why do that?

I understand the benefit is to 'move' the data instead of copy it, but where I'm lost here is when I try to piece together what happens to each object/parameter at a memory level.

Sorry if this sounds confusing, talking about it is even confusing myself.

EDIT:

In summary, I understand the 'why' of the copy constructors and move constructors... I just don't understand the 'how'.

Upvotes: 1

Views: 91

Answers (3)

Tony Delroy
Tony Delroy

Reputation: 106236

... (x + y);

Let's assume Short-String-Optimisation is not in play - either because the string implementation doesn't use it or the string values are too long. operator+ returns by value, so has to create a temporary with a new buffer totally unrelated to the x and y strings...

[ string { const char* _p_data; ... } ]
                           \
                            \-------------------------(heap)--------[ "hello world!" ];

Sans optimisation, that's done to prepare the argument for the string constructor - "before" considering what that constructor will do with the argument.

string b(x + y);

Here the string(string&&) constructor is invoked, as the compiler understands that the temporary above is suitable for moving from. When the constructor starts running, its pointer to text is uninitialised - something like the diagram below with the temporary shown again for context:

[ string { const char* _p_data; ... } ]
                           \
                            \-------------------------(heap)--------[ "hello world!" ];


[ string b { const char* _p_data; ... } ]
                           \
                            \----? uninitialised

What the move constructor for b then does is steal the existing heap buffer from the temporary.

                          nullptr
                         /
[ string { const char* _p_data; ... } ]

                             -------------------------(heap)--------[ "hello world!" ];
                            /
                           /
[ string b { const char* _p_data; ... } ]

It also needs to set the temporary's _p_data to nullptr to make sure that when the temporary's destructor runs it doesn't delete[] the buffer now considered to be owned by b. (The move constructor will "move" other data members too - the "capacity" value, either a pointer to the "end" position or a "size" value etc.).

All this avoids having b's constructor create a second heap buffer, copy all the text over into it, only to then do extra work to delete[] the temporary's buffer.

Upvotes: 1

OmnipotentEntity
OmnipotentEntity

Reputation: 17131

What's going on is a complex object will normally not be entirely stack based. Let's take an example object:

class String {
public:
  // happy fun API
private:
  size_t size;
  char* data;
};

Like most strings, our string is a character array. It essentially is an object that keeps around a character array and a proper size.

In the case of a copy, there's two steps involved. First you copy size then you copy data. But data is just a pointer. So if we copy the object then modify the original, the two places are pointing to the same data, our copy changes. This is not what we want.

So instead what must be done is to do the same thing we did when we first made the object, new the data to the proper size.

So when we're copying the object we need to do something like:

String::String(String const& copy) {
  size = copy.size;
  data = new int[size];
  memcpy(data, copy.data, size);
}

But on the other hand, if we only need to move the data, we can do something like:

String::String(String&& copy) {
  size = copy.size;
  data = copy.data;
  copy.size = 0;
  copy.data = nullptr; // So copy's dtor doesn't try to free our data.
}

Now behind the scenes, the pointer was just kinda... passed to us. We didn't have to allocate any more information. This is why moves are preferred. Allocating and copying memory on the heap can be a very expensive operation because it's not happening locally on the stack, it's happening somewhere else, so that memory has to be fetched, it might not be in cache, etc.

Upvotes: 2

John Zwinck
John Zwinck

Reputation: 249592

(x + y) gives you a string value. You want to store it in b without copying it. This was made possible long before C++11 and move semantics, by the Return Value Optimization (RVO).

Upvotes: 0

Related Questions