cpp-move-semantics

What is and how to use move semantics in C++

  • 5 min

The Move Semantics is the ability to transfer resources instead of copying them when an object no longer needs to keep them.

In the previous post we learned to distinguish between L-values (permanent objects) and R-values (disposable temporaries).

Now we’ll see why this distinction is so important in C++. We’re going to talk about move semantics.

Until C++11, if you wanted to pass a complex object (like a std::vector or a std::string) from one place to another, it almost always involved making a copy. And copying is expensive. It requires allocating new memory and copying bits one by one.

But what if the original object is a temporary that will be destroyed immediately after? Copying it is an absurd waste.

Move semantics allows transferring the resources of a temporary object instead of copying them.

Copying vs moving

To understand it, we have a class that manages a pointer to a heavy resource in memory.

  • Copy: It’s like wanting to move houses and, instead of taking your furniture, you build an identical house, buy identical furniture, and then burn down the old house. Slow and costly.
  • Move: It’s simply changing the address on your ID. The house is the same, the furniture is the same, only the owner has changed. Fast and cheap.

The move constructor

To implement this, C++ allows us to define a special constructor: the Move Constructor.

This constructor does not receive a const T& (const reference), but a T&& (R-value reference). That is, it will only activate when we pass it a temporary object.

Let’s see it with a simplified Buffer class:

class Buffer {
private:
    int* data;
    size_t size;

public:
    // Normal constructor
    Buffer(size_t n) : size(n), data(new int[n]) { }

    // Destructor
    ~Buffer() { delete[] data; }

    // 1. COPY CONSTRUCTOR (Slow)
    // Activated by: Buffer b2 = b1;
    Buffer(const Buffer& other) : size(other.size) {
        std::cout << "Copying...\n";
        data = new int[size]; // Allocate new memory
        std::copy(other.data, other.data + size, data); // Copy the data
    }

    // 2. MOVE CONSTRUCTOR (Fast)
    // Activated by: Buffer b2 = Buffer(10); (Temporaries)
    Buffer(Buffer&& other) noexcept : data(other.data), size(other.size) {
        std::cout << "Moving...\n";
        
        // CRITICAL: Leave the original object in an empty state
        other.data = nullptr; 
        other.size = 0;
    }
};
Copied!

What happens in the move constructor

Notice what we do in block 2:

  1. We steal the pointer: data(other.data). We copy the memory address, not the content. Now our object points to the data that already existed.
  2. We nullify the source: other.data = nullptr. This is essential.

If we didn’t set the source to nullptr, when the temporary object (other) gets destroyed (which will happen almost immediately), its destructor will do delete[] data and erase the memory we just stole. Disaster!

By setting it to nullptr, the temporary’s destructor does nothing (doing delete on a null is safe), and we keep the data.

Move assignment operator

Just like we have a constructor, we also need a move assignment operator (operator=). The logic is identical, but we must remember to free our own memory before stealing the new one (if we had any).

Buffer& operator=(Buffer&& other) noexcept {
    if (this != &other) {
        // 1. Free our current resources
        delete[] data;

        // 2. Steal the resources from the other
        data = other.data;
        size = other.size;

        // 3. Leave the other empty
        other.data = nullptr;
        other.size = 0;
    }
    return *this;
}
Copied!

Notice that we mark these functions as noexcept. This is a good practice in C++. If the move throws exceptions, containers like std::vector might decide not to use your move optimization for safety.

The rule of five

You probably know the classic C++ “Rule of Three” (if you define a destructor, copy, or assignment, you must define all three).

With modern C++, this evolves into the rule of five. If you manage resources manually, you must define (or explicitly delete) these five operations:

  1. Destructor
  2. Copy Constructor
  3. Copy Assignment Operator
  4. Move Constructor
  5. Move Assignment Operator

If you don’t, the compiler will try to generate them for you, but if you have raw pointers, it will probably get it wrong (copying pointers instead of data, etc).

When is it used automatically?

The compiler is smart. If it sees that the source object is an R-value (a temporary), it will automatically use the move constructor.

Buffer createBuffer() {
    return Buffer(100);
}

int main() {
    // Case A: Copy (because b1 is an L-value)
    Buffer b1(100);
    Buffer b2 = b1; 
    
    // Case B: normally there is copy elision; if not, it may move
    Buffer b3 = createBuffer(); 
}
Copied!

In case B, copy elision is normally applied, and the object is constructed directly in b3. If it cannot be applied, the result may be moved without copying all the data.

What if I want to move an L-value?

Sometimes we have a variable with a name (b1), but we know that we won’t use it anymore. We want to pass it to another function so that it keeps its resources.

If we do function(b1), the compiler will use the copy, because b1 is an L-value and it wants to protect it.

How do we tell the compiler “Hey, treat b1 as if it were a temporary, allow me to move it”? For this, there is a function in the standard library.