Move Semantics and Ownership Transfer
Object Oriented Programming and RAII

4.3 Move Semantics and Ownership Transfer

Copying data is one of the most expensive operations in systems programming. When you copy a class containing dynamic heap memory, C++ performs a deep copy: allocating new memory on the heap and copying every single byte of data. Move semantics (introduced in C++11) allows you to steal/transfer the ownership of resources instead of copying them, yielding massive speedups.

Let's look at the copy constructor problem, rvalue references, and implementing move constructors.

Deep Copies: Printing Encyclopedias

If you copy a class that manages dynamic resources, the default copy constructor copies the pointer address (shallow copy), leading to two pointers referencing the same heap memory. When they exit scope, both destructors will call delete on the same address, causing a double free crash.

To prevent this, you must write a copy constructor that performs a deep copy, which copies the actual heap data. But deep copies are extremely slow:

* The Encyclopedia Metaphor: Copying a large object is like printing an entire new set of encyclopedia volumes just to move them to another shelf. It takes time, ink, and paper. Move semantics is like taking the labels off the old shelf and sticking them onto the new shelf. The books stay in the same place; you just transfer ownership.

Move Constructors: Stealing Pointers

To support move operations, C++11 introduced rvalue references (written as type&&). Rvalue references bind to temporary variables that are about to be destroyed. By writing a move constructor, we can steal the temporary object's pointer, copy it to our new object, and set the temporary object's pointer to nullptr so its destructor does not delete our stolen heap memory:

#include <iostream>

class DynamicBuffer {
    int* data;
    unsigned int size;
public:
    // Standard Constructor
    DynamicBuffer(unsigned int s) : size{s}, data{new int[s]} {}

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

    // 1. Copy Constructor (Deep Copy)
    DynamicBuffer(const DynamicBuffer& other) : size{other.size}, data{new int[other.size]} {
        for (unsigned int i{0}; i < size; ++i) {
            data[i] = other.data[i];
        }
    }

    // 2. Move Constructor (Shallow Transfer / Theft)
    DynamicBuffer(DynamicBuffer&& other) noexcept : data{other.data}, size{other.size} {
        other.data = nullptr; // Nullify original pointer to prevent double free!
        other.size = 0;
    }

    // 3. Move Assignment Operator (Self Assignment Check & Cleanup)
    DynamicBuffer& operator=(DynamicBuffer&& other) noexcept {
        if (this != &other) { // Prevent self assignment
            delete[] data;    // Free existing dynamic resources first!
            data = other.data; // Steal the pointers
            size = other.size;
            other.data = nullptr; // Reset original pointer to prevent double free
            other.size = 0;
        }
        return *this;
    }
};
Implementing a dynamic buffer with move constructor and move assignment.

The Rule of Five

If your class manages resources manually, you must define or explicitly default five key functions to ensure correct copy and move behavior. If you write one, you should write them all:

* Destructor: To free heap resources.

* Copy Constructor: To duplicate resources safely.

* Copy Assignment Operator: To overwrite resources safely.

* Move Constructor: To steal resources from temporary objects.

* Move Assignment Operator: To overwrite resources by stealing from temporary objects.

Using std::move casts an object to an rvalue reference, telling the compiler it is safe to invoke the move constructor instead of the copy constructor.

The Rule of Zero

Writing destructors, copy operations, and move operations manually is highly error prone. In modern C++, the recommended guideline is the Rule of Zero: design your classes so they do not manage raw memory pointers directly. Instead, use standard library types (such as std::string, std::vector, or smart pointers) to manage resource lifetimes automatically. When you do this, you do not need to write any of the Rule of Five functions yourself; the compiler will generate safe defaults automatically.

#include <string>
#include <vector>

class SafePlayer {
    std::string name; // Managed automatically by std::string
    std::vector<int> scores; // Managed automatically by std::vector
public:
    SafePlayer(std::string n) : name{n} {}
    // Zero custom destructors, copy, or move operations needed!
    // The compiler automatically generates optimal defaults for all five operations.
};
Applying the Rule of Zero using standard containers.
Finished reading this lesson?