Move Semantics: The Basics

Move Semantics

Move semantics was a new addition to C++ that arrived with C++ 11. Newer language standards have continued to expand and improve it. The feature is quite simple, but often misunderstood. I’m often reminded of that when I’m interviewing programmers.

Who will Find this Article Useful?

In this article I will cover all the basics of move semantics. This information will be useful for people that have never used move semantics but for people that are somehow familiar with the topic and use cases but don’t necessary use move semantics very often. The article will also be useful for people that want to dig deeper into the feature. And it will be useful for people that want to refresh their memory.

To benefit from this article to the fullest, you will need a good understanding of how copy constructors and assignment operators work, and you need to have a basic understanding of types and type casting.

1 – The case for Move Semantics

Before we look at the definition of move semantics, let us look at an example that will set up the premise of why move semantics exist.

1.1 – Setting the example

Let us assume that we are going to implement a struct that represents a memory block on the heap. The only thing this struct is responsible for is allocating the memory on construction and deallocating it on destruction. We can imagine that this struct is used by a complex memory allocation system. Let’s say that system is managing many instances of our struct, in other words, it is managing many memory buffers.

Here is the declaration of our struct:

struct MemoryBuffer
{
     // Data
     char* m_Buffer;
     size_t m_SizeInBytes;
    
    // Constructor
    MemoryBuffer(const size_t inSizeInBytes)
    : m_Buffer(new char[inSizeInBytes]), 
      m_SizeInBytes(inSizeInBytes)
    {
    }

    // Destructor
    ~MemoryBuffer()
    {
        delete[] m_Buffer;
    }

    // Copy constructor
    MemoryBuffer(const MemoryBuffer& inSrc)
     : m_Buffer(new char[inSrc.m_SizeInBytes]),
       m_SizeInBytes(inSrc.m_SizeInBytes)
    {
        memcpy(m_Buffer, inSrc.m_Buffer, m_SizeInBytes);
    }

    // Copy assignment operator
    MemoryBuffer& operator=(const MemoryBuffer& inRhs)
    {
        if (this != &inRhs)
        {
            delete[] m_Buffer;
            m_Buffer = new char[inRhs.m_SizeInBytes];
            m_SizeInBytes = inRhs.m_SizeInBytes;
            memcpy(m_Buffer, inRhs.m_Buffer, m_SizeInBytes);
        }
        return *this;
    }
};

We need to keep the example simple. And variables that could be needed for a robust solution, like used or free bytes and masks of all sorts, are out of the scope of this example.

1.2 – Using the Memory Buffer

Now let us also imagine that there is a free utility function that creates an array of buffers and returns the array. We can assume that this function is used by the complex memory allocation system in order to pre-allocate memory pools of different sizes.

vector<MemoryBuffer> CreateMemoryPool
(
    const size_t inNrOfBuffers, 
    const size_t inBufferSize
)
{
    vector<MemoryBuffer> pool;
    pool.reserve(inNrOfBuffers);

    for (size_t i = 0; i < inNrOfBuffers; ++i)
        pool.push_back(MemoryBuffer(inBufferSize));

    return pool;
}

Before C++ 11, no one would write this function in this way. Output parameters as pointer or reference will be used instead. This will make the function more complicated to implement, maintain and use. To explain why this was the case, let us have a look of how the “CreateMemoryPool” function is used. Let us assume that a bunch of pools of various sizes are created by the memory system in an “InitializePools” member function.

MemoryAllocationSystem::Initialize()
{
    // m_PagePool member variable is of type vector<MemoryBuffer>
    m_PagePool = CreateMemoryPool(10, 4096);

    // Create other pools....
    m_HalfPagePool = CreateMemoryPool(10, 2048);
    m_QuarterPagePool = CreateMemoryPool(10, 1024);
}

We will assume that we are not using any C++ 11 or newer additions to the language. We will also assume that for whatever reason the compiler will decide not to use Copy Elision to omit the copy of the return value of the “CreateMemoryPool(…)” function.

With all of these assumptions in play, calling the “CreateMemoryPool(…)” function will create a temporary return variable that will be used to assign to the member variables via the copy assignment operator of the std::vector. In other words, we are going to do a deep copy. This is very suboptimal way to write such code.

1.3 – The problem: deep copies can be very expensive

The problem with this scenario is that the local pool variable in the “CreateMemoryPool(…)” function, that stored the result of the function, will be copied into the m_PagePool and then destroyed. We are wasting a lot of cycles that way by executing extra instructions. We are also wasting cycles by allocating and deallocating memory (which is quite slow, generally speaking).

Before C++ 11, we would often reach for one of the following to minimize the allocations/deallocations caused by local and temporary variables:

  • Pass a pointer or a reference to the m_PagePool as input to the “CreateMemoryPool(…)” and let the function directly push back into the m_PagePool. In this way “CreateMemoryPool(…)” can return void, and the function is going to work directly on the variable that will store the final result of the function. And in this way we avoid copies. The first drawback is the extra parameter that is passed to the function, adding complexity. For example, doing this creates ambiguity when it comes to responsibility for the input state of that vector. Is it the callers responsibility to ensure that the vector is empty when invoking the function or will the function itself clear the vector? The second is the fact that passing a non-constant pointer or reference of the internal variable m_PagePool makes our code less safe because anyone can write code that does something bad to m_PagePool and the caller of “CreateMemoryPool(…)” loses all guarantees.
  • We can change the result of the function to return a vector of pointers to memory buffers, like so: vector<MemoryBuffer*>. This way we only copy the pointers when the “CreateMemoryPool(…)” function returns and there is no deep copying going on and no extra allocations/deallocations. The drawback to this is now the owner of the m_PagePool needs to worry about manually deallocating the pointers held by the vector, because the vector won’t do it in it own. Of course, we can use a vector of some of smart pointers to automate that part as well, but that also adds complexity.

1.4 – Move semantics solves the possible performance issue with deep copies

What we really want to do is keep the “CreateMemoryPool(…)” function implementation as it is, simple and safe, and move the result of the function directly into the m_PagePool variable without doing a deep copy, as if we are transferring water from one glass directly into another. Furthermore, we are doing even more copying by using the “push_back” function of the vector class and we want the push_back to also behave as if we are transferring water from one glass directly into another. C++ 11 and move semantics allows us to do exactly that. In the following sections, we are going to explore what move semantics are.

2 – Definition of Move Semantics

Move semantics are typically used to “steal” the resources held by another variable of the same type (e.g. pointers to dynamically-allocated objects, file descriptors, TCP sockets, I/O streams, running threads, etc.) and leave the source variable in some valid state, rather than making a deep copy of them.

The two most common use cases for move semantics are :

  • Performance: converting an expensive deep copy operation into a cheap move, like moving the data from a source string into a target string without having to allocate/deallocate.
  • Ownership semantics: Implementing user defined data types for which copying is not allowed but moving is, like the std::unique_ptr.

3 – Lvalues, Rvalues and Xvalues

To understand how move semantics work under the hood, we need to define a few other terms. Let’s start by looking at a pseudo-code that represents an assignment statement:

L = R

The left side of the statement is what historically is called Lvalue. And the right side of the statement is what historically is called Rvalue. Of course, because of the general nature of C++ things are often more complicated. Rvalues can appear on the left side, and Lvalues on the right side. Why that is the case is not relevant for this article.

3.1 – Easy way to differentiate between Lvalues and Rvalues

You might think that the fact that Lvalues and Rvalues can appear on either side is very confusing. Fortunately, there is a very simple rule we can use to remove all this confusion. A rule that will allow us to easily classify Lvalues and Rvalues in C++.

If a variable has an identifier that you have chosen yourself, then it is an Lvalue. And if a variable does not have a name that you have deliberately selected, then it is an Rvalue. To demonstrate this rule, let’s look at the following code snippets:

int sum = 0; // sum is Lvalue because it has a name/identifier that we decided on
sum = 1 + 2 + 3; // the temporary variable that stores the result of 1 + 2 + 3 
                            // is an Rvalue because it has no name

Here is another example:

int Add123()
{
    return 1 + 2 + 3;
}

sum = Add123(); // the temporary variable that is the result of calling Add123() 
                            // is an Rvalue because it has no name 

3.2 – Double ampersands(&&) and the new operators

Prior to C++ 11, Rvalues and Lvalues were indistinguishable. The following example code would trigger the copy constructor and the copy assignment operator of the std::vector.

vector<MemoryBuffer> pool1;
vector<MemoryBuffer> pool2;

// Lvalue = Rvalue
pool1= CreateMemoryPool(10, 4096); // Copy assignment

// Lvalue = Lvalue
pool1= pool2; // Copy assignment

// Lvalue = Rvalue
vector<MemoryBuffer> pool3 = CreateMemoryPool(10, 4096); // Copy constructor

// Lvalue = Lvalue
vector<MemoryBuffer> pool4 = pool3 ; // Copy constructor

This is the Copy Constructor and the Copy Assignment Operator that would have been used in both cases:

vector(const vector& inSrc) 
{
    //... implementation
}

vector& operator=(const vector& inRhs)
{
    //... implementation
}

To solve this problem, C++ 11 introduced Rvalue references, denoted by a double ampersands (&&). With this addition, we can now have two different copy constructors and assignment operators. One for Lvalues and one for Rvalues.

The following code snippet shows the declaration of the copy and the move constructors:

// Copy constructor
vector(const vector& inSrc)  // inSrc is an Lvalue reference
{
    //... implementation
}

// Move constructor
vector(vector&& inSrc) // inSrc is an Rvalue reference
{
    //... implementation
}

The following code snippet shows the declaration of the copy and the move assignment operators:

// Copy assignment operator
vector& operator=(const vector& inRhs) // inRhs is an Lvalue reference
{
    //... implementation
}

// Move assignment operator
vector& operator=(vector&& inRhs) // inRhs is an Rvalue reference
{
    //... implementation
}

We can now revisit the example from above. And we can examine how in the cases where we have an Rvalue on the right hand side, the new move constructor and move assignment operators will be called instead of the copy constructor and copy assignment.

vector<MemoryBuffer> pool1;
vector<MemoryBuffer> pool2;

// Lvalue = Rvalue
pool1= CreateMemoryPool(10, 4096); // <=== Move assignment

// Lvalue = Lvalue
pool1= pool2; // Copy assignment

// Lvalue = Rvalue
vector<MemoryBuffer> pool3 = CreateMemoryPool(10, 4096); // <=== Move constructor

// Lvalue = Lvalue
vector<MemoryBuffer> pool4 = pool3 ; // Copy constructor

4 – Declaring Move Constructor and Move Assignment Operator

Now we know that C++ 11 introduced Rvalue references, denoted by a double ampersands (&&). And we know that we can declare a move constructor and a move assignment operator using the Rvalue type. Let’s declare them for our MemoryBuffer struct:

struct MemoryBuffer
{
    // Data
    char* m_Buffer;
    size_t m_SizeInBytes;

    // Constructor
    MemoryBuffer(const size_t inSizeInBytes);

    // Destructor
    ~MemoryBuffer();

    // Copy constructor
    MemoryBuffer(const MemoryBuffer& inSrc);

    // Move constructor  <=== NEW CONSTRUCTOR
    MemoryBuffer(MemoryBuffer&& inSrc);

    // Copy assignment operator
    MemoryBuffer& operator=(const MemoryBuffer& inRhs);

    // Move assignment operator <=== NEW ASSIGNMENT OPERATOR
    MemoryBuffer& operator=(MemoryBuffer&& inRhs);
}

5 – Defining Move Constructor and Move Assignment Operator

We now know how to declare the move constructor and move assignment operator. It is now time to define them and actually implement the useful move semantics.

// Move constructor
MemoryBuffer(MemoryBuffer&& inSrc)
: m_Buffer(inSrc.m_Buffer)
, m_SizeInBytes(inSrc.m_SizeInBytes)
{
    inSrc.m_Buffer = nullptr;
    inSrc.m_SizeInBytes = 0;
}

// Move assignment operator
MemoryBuffer& operator=(MemoryBuffer&& inRhs)
{
    if (this != &inRhs)
    {
        m_Buffer = inRhs.m_Buffer;
        m_SizeInBytes = inRhs.m_SizeInBytes;
        inRhs.m_Buffer = nullptr;
        inRhs.m_SizeInBytes = 0;
    }
    return *this;
}

What we are doing in the move constructor and the move assignment is identical, except we also need to take care of self assignment in the move assignment operator. Let’s examine what we are doing in the move constructor:

  1. In the initializer list, we are copying the source m_Buffer pointer into our own internal m_Buffer pointer. This is a simple pointer copy and not a deep copy. At this point both the source and the internal m_Buffer pointers point to the same memory address.
  2. Then, also in the initializer list, we are copying the variable holding the size of the source buffer into the internal m_SizeInBytes variable.
  3. Then, in the move constructor body, we set the source buffer pointer to nullptr. Effectively leaving the source pointer in a valid state, but also at this point completing the process of stealing the resource. After this the internal m_Buffer pointer points to where the source m_Buffer pointer used to point.
  4. Finally, we reset the m_SizeInBytes of the source to 0, effectively leaving the source MemoryBuffer in a valid state.

For completeness, here is the the entire MemoryBuffer struct:

struct MemoryBuffer
{
     // Data
     char* m_Buffer;
     size_t m_SizeInBytes;
    
    // Constructor
    MemoryBuffer(const size_t inSizeInBytes)
    : m_Buffer(new char[inSizeInBytes]), 
      m_SizeInBytes(inSizeInBytes)
    {
    }

    // Destructor
    ~MemoryBuffer()
    {
        delete[] m_Buffer;
    }

    // Copy constructor
    MemoryBuffer(const MemoryBuffer& inSrc)
     : m_Buffer(new char[inSrc.m_SizeInBytes]),
       m_SizeInBytes(inSrc.m_SizeInBytes)
    {
        memcpy(m_Buffer, inSrc.m_Buffer, m_SizeInBytes);
    }

    // Move constructor
    MemoryBuffer(MemoryBuffer&& inSrc)
    : m_Buffer(inSrc.m_Buffer),
      m_SizeInBytes(inSrc.m_SizeInBytes)
    {
        inSrc.m_Buffer = nullptr;
        inSrc.m_SizeInBytes = 0;
    }

    // Copy assignment operator
    MemoryBuffer& operator=(const MemoryBuffer& inRhs)
    {
        if (this != &inRhs)
        {
            delete[] m_Buffer;
            m_Buffer = new char[inRhs.m_SizeInBytes];
            m_SizeInBytes = inRhs.m_SizeInBytes;
            memcpy(m_Buffer, inRhs.m_Buffer, m_SizeInBytes);
        }
        return *this;
    }

    // Move assignment operator
    MemoryBuffer& operator=(MemoryBuffer&& inRhs)
    {
        if (this != &inRhs)
        {
            m_Buffer = inRhs.m_Buffer;
            m_SizeInBytes = inRhs.m_SizeInBytes;
            inRhs.m_Buffer = nullptr;
            inRhs.m_SizeInBytes = 0;
        }
        return *this;
    }
};

6 – Move Semantics in Action

With the new knowledge we acquired about Lvalues and Rvalues, as well as with the implementation of the move constructor and move assignment operator, we can now revisit the example we started with and see move semantics in action.

First, let us make a small change in the “CreateMemoryPool(…)” function.

vector<MemoryBuffer> CreateMemoryPool
(
    const size_t inNrOfBuffers, 
    const size_t inBufferSize
)
{
    vector<MemoryBuffer> pool;
    pool.reserve(inNrOfBuffers);

    for (size_t i = 0; i < inNrOfBuffers; ++i)
        pool.emplace_back(MemoryBuffer(inBufferSize)); // << used to be push_back

    return pool;
}

To take advantage of the new move semantics and variadic templates, in C++ 11 a new version of the vector::push_back was added to the standard library. I recommend that you check the documentation, but in a nutshell, emplace_back will construct the new MemoryBuffer right in the memory location where it will be stored by the vector and in this way reducing the need to deep copy.

And finally, let us look at the “InitializePools” member function. Notice that the code is unchanged, but the compiler will call the move assignment operator and avoid deep copy and the extra allocations/deallocations, that are quite slow.

MemoryAllocationSystem::Initialize()
{
    // Note that move assignment operator is called here instead of copy assignment
    // Lvalue = Rvalue
    m_PagePool = CreateMemoryPool(10, 4096); 

    // Create other pools....
    m_HalfPagePool = CreateMemoryPool(10, 2048);
    m_QuarterPagePool = CreateMemoryPool(10, 1024);
}

7 – Automatic vs. Manual Move

Looking at the “MemoryAllocationSystem::Initialize()” function, we can easily conclude that if move semantics were available to the compiler and if the types in question (std::vector and MemorBuffer in this case) supports move semantics, the compiler will detect the opportunity to move instead of copy. The reason for this is that the compiler will be able to see that the local variable created and returned by the “CreateMemoryPool(…)” is in fact an Rvalue.

The compiler is unable to detect every move opportunity. We can create a situation where we, as programmers, might have the intent to move the content of one array into another array (for whatever reasons) but the compiler will not be able to automatically detect our intent to move and will fallback to a deep copy. This of course happens when we have the intention to move an Lvalue into another Lvalue. Here is an example:

vector<MemoryBuffer> pool1 = CreateMemoryPool(10, 4096);
vector<MemoryBuffer> pool2 = CreateMemoryPool(10, 2048);
// ... some code
// ... more code
// ... and now we want to move pool1 into pool2
pool2 = pool1;

By default, the compiler will correctly assume that we ask for a deep copy. And if we really want to move, then we need to help the compiler a bit and turn the right hand Lvalues into Rvalue. To explicitly tell the compiler what our intent is, C++ 11 introduced a helper function: std::move. To utilize this new helper function, we can change our example to the following:

vector<MemoryBuffer> pool1 = CreateMemoryPool(10, 4096);
vector<MemoryBuffer> pool2 = CreateMemoryPool(10, 2048);
// ... some code
// ... more code
// ... and now we want to move pool1 into pool2
pool2 = std::move(pool1);

All std::move does is a cast. It casts the “pool1” variable from an Lvalue reference (&) to an Rvalue reference (&&). Doing so tells the compiler which assignment operator it should call, because the variable type changes. And of course this is all based on standard function overloading rules. In its core, the assignment operator is a function, which is overloaded for two different input types (Lvalue reference and Rvalue reference).

8 – Xvalue

The following is a bit of an extra context and not absolutely necessary for this article, but I would rather mention it. The “pool1” variable, in the example above, is technically an Xvalue after the std::move is executed. For all intent and purposes, you can treat it as an Lvalue, but the language implementers need a technical term for it. It means expiring value and it denotes an Lvalue object that can be reused after a move from it was executed. There are exactly three things that can be done to an Xvalue:

  • Copy assign to the Xvalue, turning it into an Lvalue.
  • Move assign to the Xvalue, turning it into an Lvalue.
  • Call the destructor when the Xvalue goes out of scope.

9 – Special Operators’ Rules

We all know that sometimes the compiler will generate constructors and other special operators on its own. The good news is that there are rules for when the compiler will do that. And now with the new move constructor and move assignment operator, these rules have been updated. For completeness, it is worth mentioning the most important rules here.

Rule 1: Default move operations are generated only if no copy operations or destructor is user defined. This is because C++ assumes that if you need a destructor or you manually implemented copy operations, or both, then you are dealing with some sort of resource that needs special treatment. And because it is a resource, you need to implement the move operations yourself because you, as the expert, know best how this resources should behave.

Rule 2: Default copy operations are generated only if there is no user defined move operations. The reasons here are are the same as in Rule 1. Note that =default and =delete count as user defined.

Rule 3: You don’t need to implement move semantics or even copy semantics manually if all of the internal member variables are of types that are movable or copyable. For example, if we were to change the “char* m_Buffer” in our MemoryBuffer class to “unique_ptr<char> m_Buffer”, then we do not need to implement the move semantics ourselves, because the unique_ptr<T> class already supports move semantics.

10 – Parameters Convention

Move semantics is an optimization that is only applicable to some use cases. In general prefer simple and conventional ways of passing information. The following table illustrates an accepted convention for passing parameters and what types you should use. When in doubt, always check it.

Move Semantics and Parameters Convention
Parameters Convention Table

11 – Forwarding a Reference

For completeness, I need to cover one last topic. One more function was introduced together with std::move in C++ 11. And it was std::forward.

std::forward is pretty simple as it has only one use case. Its purpose is to preserve the value type, regardless if it is Lvalue or Rvalue, and pass it on. This is also called perfect forwarding. Typically a function accepting an Rvalue will attempt to move it and invalidate it, and when this is not the case, we can use std::forwad to pass on the Rvalue down the call-stack. The following example illustrates the use case.

void PrintValue(const float& inArg) 
{
    std::cout << "inArg is an lvalue" << std::endl;
}

void PrintValue(float&& inArg) 
{
    std::cout << "inArg is an rvalue" << std::endl;
}

template<typename T>
void ForwardingFunc(T&& inArg)
{
    PrintValue(std::forward<T>(inArg));
}

void main ()
{
    const float some_value = 7.0f
    ForwardingFunc(some_value); // ==> will print "inArgis is an lvalue"
    ForwardingFunc(7.0f); // ==> will print "inArgis is an rvalue"
    return 0;
}

For completeness, it is worth mentioning that In the code example above the input “T&& inArg” is called Forwarding reference. Forwarding references are a special kind of references that preserve the value category of a function argument. In this case calling “std::forward<T>(inArg)” will preserve the input argument and will forward it as Lvalue it if the input is an Lvalue, or it will forward it as Rvalue if the input is an Rvalue.

12 – Conclusion

In general, the concept of move semantics is quite simple and as programmers we should learn how and when to use it. The concept is built based on two fundamental C++ concepts: types and function/operator overloading. The following is a list of takeaway:

  • Pre-C++ 11, value semantics sometimes lead to unnecessary and possibly expensive deep copy operations.
  • C++ 11 introduces Rvalue references to distinguish from Lvalue references. And it also introduces std::move to cast from Lvalue to Rvalue.
  • From C++ 11 on, temporary variables are treated as Rvalues.
  • The two most common use cases for move semantics are :
    • Performance: converting an expensive deep copy operation into a cheap move, like moving the data from a source string into a target string without having to allocate/deallocate.
    • Ownership semantics: Implementing user defined data types for which copying is not allowed but moving is, like the std::unique_ptr.
  • Moving POD or structs composed of PODs will not give you any benefits. To get benefits from move semantics you need some kind of resource (e.g. pointers to dynamically-allocated objects, file descriptors, TCP sockets, I/O streams, running threads, etc.).

13 – Source Code

The source code containing the MemoryBuffer example is here (under MoveSemantics). You do not need a Github account to download it. When you open the repository, click on the Code button and choose Donwload ZIP.

14 – Credit

Special thanks to my colleagues for spending their personal time proofreading, challenge this article and helping me to make it better. It is a pleasure working with every one of you!

  • Anton Andersson: Senior Gameplay Programmer at IO Interactive.
  • Nils Iver Holtar: Lead AI Programmer at IO Interactive.

Please consider sharing this post if you find the information useful.