Move semantics in C++

Consider the following simple string class:

class String
{
public:
    String()
        : data_(strdup_("")), len_(0)
    {
    }

    String(const char* str)
        : data_(strdup_(str)), len_(std::strlen(data_))
    {
    }

    String(const char *str, size_t len)
        : data_(strndup_(str, len)), len_(len)
    {
    }

    String(const String& other)
        : data_(strdup_(other.data_)), len_(other.len_)
    {
    }

    ~String()
    {
        delete[] data_;
    }

    String& operator=(String other)
    {
        std::swap(data_, other.data_);
        return *this;
    }

    String& operator+=(const String& other)
    {
        size_t len = len_ + other.len_;
        char *data = new char[len + 1];
        std::strcpy(data, data_);
        std::strcat(data, other.data_);
        delete[] data_;
        data_ = data;
        len_ = len;
        return *this;
    }

    String substring(size_t start, size_t len)
    {
        if (start < len_ - 1 && len <= len_ - start) {
            return String(&data_[start], len);
        }
        return String();
    }

    friend String operator+(String lhs, const String& rhs);
    friend std::ostream& operator<<(std::ostream& os, const String& str);

private:
    char* data_;
    size_t len_;
};

The important things to note are:

  • It manages a dynamically allocated block of memory, so it needs to observe the Rule of Three
  • In addition to its constructors, it has two functions that return an object: operator+(), and substring()

Now consider the following uses of the class:

int main()
{
    String a = "The quick brown fox jumps over";
    String b = " the lazy dog";

    String x = a;                  // Copy an existing object
    String y = a + b;              // Create a new object by addition and then copy it
    String z = a.substring(16, 3); // Create a new object by function call and then copy it

    std::cout << x << "\n";
    std::cout << y << "\n";
    std::cout << z << "\n";
}

There are 3 copying operations in this code. In the first, we are copying the instance a, so we naturally expect to be able to use a again after the copy has been made.
In the other two though, temporary objects are created (by operator+() and substring respectively), and there is no way we could use them before they are destroyed at the end of the expression. These temporary objects are called rvalues because they only occur on the right-hand side of an expression, and can never be on the left-hand side of one.

As these objects can never be used, it’s wasteful and unnecessary for them to go through the same construction and destruction process as objects we do use. In particular, it shouldn’t be necessary to go through the process of copying the data_ member in the copy constructor, only for it to be deleted immediately after the copy is made.

What move semantics provide, amongst other things, is a way of writing a copy constructor that behaves differently when the object being constructed is an rvalue, called a move constructor. Below is a move constructor for our string class:

String(String&& other)
{
    data_ = other.data_;
    other.data_ = nullptr;
}

Note the &&, which indicates an rvalue reference variable. It is this that makes the constructor a move constructor. This constructor will be called in the situations like those above where the compiler can tell that the object being copied is an rvalue. In this case we can simply steal the data_ member from the soon-to-be-destroyed other object by making it the data_ member of the new object, and then setting it the other object’s member to nullptr so that the block won’t be deleted when the other object goes out of scope. This makes the whole process of copy initialisation much more efficient when the object being copied is an rvalue.

There is a lot more to rvalues and move semantics than this, but as a first step it is well worth considering adding a move constructor to a class that has methods that return an object, as these objects are quite likely to be constructed as rvalues in practice.