C++ Move Semantics Tutorial

With the Introduction of Lvalues and Rvalues in C++11, the concept of Move Semantics was brought about as a way of “moving” objects instead of “copying” them. What are Lvalues and Rvalues though? Why is it a bad idea to be copying objects instead of moving them? What’s the benefit of using Move Semantics in C++, and where should they be used?

In this C++ Move Semantics tutorial, we will do our best to answer all of these questions.


Background to Move Semantics in C++

The basis behind Move Semantics is rvalues and lvalues, so it’s best to take a brief look at them before moving forward.

What are Lvalues and Rvalues?

An lvalue is something that has a memory location associated with it. Something to which an rvalue can be assigned. Examples of an value include a simple variable, to which a value like “10” could be assigned.

int var = 10;

Conversely, an rvalue is something that has no memory associated with it, and cannot be assigned a value. Examples of rvalues include a simple constant like 10. Now you obviously can’t do something like 10 = var, right?

The reason why lvalues and rvalues simply don’t mean left and right values (as people often mistakenly assume), is because we can do something like this:

int var1;
int var2;

var2 = var1;

In the above example, we have an lvalue on both the left and right side.

Let’s take a look at one more example to really get the concept across.

The below example shows two variables being added together, and assigned to a third variable. Do you see an rvalue here?

var3 = var1 + var2

There actually is an rvalue, and it is the following expression, var1 + var2. While each of these two variables separately count as lvalues, summing them like this produces an rvalue.

If you want to do a quick check as to whether something is an rvalue or not, try to imagine assigning it a value.

Ask yourself, does the following code work?

var1 + var2 = var3

No, right? There is your answer.


Importance of Move Semantics

So what problem does Move Semantics set out to solve? Well, imagine you are passing an Object with has some dynamically allocated memory. Now what typically happens, is that when passing it into a function, it gets “copied” over, using the Copy Constructor.

If you remember the whole concept of Deep and Shallow Copy Constructors, you might remember how we have to allocate new dynamic memory every time we copy such an object. And every time we allocate we new memory, it needs to be de-allocated as well.

But this presents a problem, because we just allocated memory twice, first when we declared the object to be copied over, and the second when we were copying it to it’s target destination.

So you might wonder, is there a way to do this, without having to allocate and deallocate memory twice? Can we not just “move” the memory into the target destination?

Well this is where the concept of Move Semantics in C++ comes in.

For a full, more in-depth look at rvalues and lvalues with examples, refer to our dedicated tutorial for it.


Creating a Custom Class

Let’s take a look at the normal “non-move semantic” version first.

First we are going to define a Custom Class that uses dynamically allocated data. This part is important, as we need to manually define the Copy Constructor, and later the Move Constructor for this Custom Class. This gives us alot of control over our Custom Datatype.

To keep things simple, we will make a String class, which dynamically allocates an array of characters. The below code is our Class definition for the String Class. As you can see, we have defined the Constructor, Copy Constructor and the Destructor. (And a utility function for printing the string out!)

class String {
    char * _data;
    int _size;

public:
    String(const char* data) {
        cout << "Created" << endl;
        _size = strlen(data);
        _data = new char[_size];
        memcpy(_data, data, _size);
    }

    String(const String& str) {
        cout << "Copied" << endl;
        _size = str._size;
        _data = new char[_size];
        memcpy(_data, str._data, _size);
    }

    void Print() const {
        for (size_t i = 0; i < _size; i++)
            cout << _data[i];
        cout << endl;
    }

    ~String() {
        cout << "Destroyed" << endl;
        delete[] _data;
    }
};

All three functions have prompts in them, which lets us know exactly which ones are being called, and in which order.

Now we are going to make a second Class, where we are going to be passing our String into.

class Person {
    String _name;

public:
    Person(const String& s) : _name(s) { }

    void PrintName() {
        _name.Print();
    }
};

See? It’s a fairly normal example right? All we are doing, is passing a String object into the Constructor for the Person class, and initializing the _name variable. You’ve all done something very similar to this before, but you did it using the std::string. The only difference here, is that we are using our own Custom String Class.


Identifying the Problem

So what’s the problem with this code? Well, let’s run it to find out, as our prompts will let us know, which functions are being called.

int main() {
    Person person(String("Coder"));
    person.PrintName();

    cin.get();
}

The following output is created.

Created
Copied
Destroyed
Coder

Now let’s try and understand this. First, we created the String object in main, hence the “Created” prompt was called in the Class Constructor. Next, in the Constructor for the Person object, the String object gets copied into the _name variable. This is where the copy constructor is called.

Now the original object that we created in the main, gets destroyed, hence the destructor is called. (Another destructor will get called once we exit from main, but we can’t see that right now due to the cin.get() function)

So as we discussed earlier, Copy Constructors are bad. Allocating memory twice is bad. Of course, this is only really matters if we are dealing with a lot of dynamic data. If it’s just a handful of integers or characters, it doesn’t really matter. But here we need a proper solution, as Strings can go upto hundreds of characters pretty easily.


Implementing Move Semantics

It’s time for move semantics to enter the scene.

The first thing we need to do, is define a Move Constructor for the String Class. It takes as parameter, an rvalue reference to the String object, which is denoted by the && operator.

Now you might wonder how this is different from the Copy Constructor.

    String(String&& str) {
        cout << "Moved" << endl;
        _size = str._size;
        _data = str._data;

        str._data = nullptr;
    }

As you can probably see, the Move function has nothing to do with allocating new memory. The whole idea behind the Move constructor, is to simply “transfer” ownership of the memory from one String object to the other.

In short, instead of allocating new memory, we had _data store a pointer to the original memory, and then we made the original String object’s _data point to nullptr. The reason for making it NULL/nullptr is so that later when the destructor gets called on the original object, then it won’t delete the memory.

So let’s summarize what just happened.

  1. We made a new function, that takes an rvalue reference, not an lvalue.
  2. Instead of allocating new memory, we simply did some pointer swapping. Basically passing control of the memory from the original object to the new one.
  3. Made the original’s pointer null, so we don’t end up with two objects sharing the same memory.

We still aren’t done though! There is a new Constructor, that we need to define for the Person Class that takes a rvalue reference.

Person(String&& s) : _name(std::move(s)) { }

You can also see us using a new function, called std::move(). As an alternative you can also cast s to String&&, but that’s a bit weird. Both this and the rvalue reference constructor are needed to put the final pieces in places.

Person(String&& s) : _name((String&&) s) { }

The alternate approach is shown above.

If you are having a little trouble wrapping your head around what’s going on in this part, it’s probably because you don’t know enough about lvalues and rvalues references. Go check out our tutorial on them, and all will be clear.


Final Output & Analysis

Now if we run the code, we get the following output.

Created
Moved
Destroyed
Coder

See? No more copying. You can go ahead and remove that Copy Constructor, because we don’t need it any longer!

And that’s the whole idea behind Move Semantics. It was quite complex, but also rather simple when you think about the base idea, which is simply to transfer memory, rather than allocate it again on the heap, which is costly.

So just how faster is the non-move semantic approach, vs Move Semantics? Well, a study I once saw showed a 70% increase in speed. Feel free to do your own research online, until we’ve come up with our own performance tests for you to see.

Here is the full code:

#include <iostream>
#include <cstring>
using namespace std;


class String {
    char * _data;
    int _size;

public:
    String(const char* data) {
        cout << "Created" << endl;
        _size = strlen(data);
        _data = new char[_size];
        memcpy(_data, data, _size);
    }

    String(String&& str) {
        cout << "Moved" << endl;
        _size = str._size;
        _data = str._data;

        str._data = nullptr;
    }

    String(const String& str) {
        cout << "Copied" << endl;
        _size = str._size;
        _data = new char[_size];
        memcpy(_data, str._data, _size);
    }

    void Print() const {
        for (size_t i = 0; i < _size; i++)
            cout << _data[i];
        cout << endl;
    }

    ~String() {
        cout << "Destroyed" << endl;
        delete[] _data;
    }
};


class Person {
    String _name;

public:
    Person(const String& s) : _name(s) { }

    Person(String&& s) : _name(std::move(s)) { }

    void PrintName() {
        _name.Print();
    }
};

int main() {
    Person person(String("Coder"));
    person.PrintName();

    cin.get();
}


This marks the end of the C++ Move Semantics Tutorial. Any suggestions or contributions for CodersLegacy are more than welcome. Questions regarding the tutorial content can be asked in the comments section below.

Subscribe
Notify of
guest
2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments