Copy and Move Semantics

The Rule of Five

When a class handles a resource that the compiler can't properly copy or destroy by itself, the five special member functions are no longer optional. They become a set. If you write one of them, you need to write the others (or explicitly delete them). This is the Rule of Five.

Consider a simple class that owns a dynamically allocated integer:

#include <iostream>

class OwnedInt
{
private:
    int* m_ptr{};

public:
    explicit OwnedInt(int value)
        : m_ptr{ new int{ value } }
    {
    }

    // Run this destructor when the object lifetime ends
    ~OwnedInt()
    {
        delete m_ptr;
    }

    int get() const
    {
        return *m_ptr;
    }
};

int main()
{
    OwnedInt a{ 42 };
    std::cout << a.get() << '\n';
    return 0;
}

Constructor: dynamically allocates an int on the heap, and saves the address of it to m_ptr. Destructor: frees this memory using delete. Because we defined a destructor, the move constructors are no longer declared by the compiler.

The copy constructors still are, but they do a "shallow copy": they just copy the pointer value.

Now two OwnedInt instances point to the same place in the heap. When both go out of scope, the same address gets deleted twice. That is undefined behavior.

The Rule of Five states that on any appearance of one of the five special members, five shall be called. The complete set for this would be:

#include <iostream>
#include <utility>

class OwnedInt
{
private:
    int* m_ptr{};

public:
    explicit OwnedInt(int value)
        : m_ptr{ new int{ value } }
    {
    }

    // 1. Destructor
    ~OwnedInt()
    {
        delete m_ptr;
    }

    // 2. Copy constructor
    OwnedInt(const OwnedInt& other)
        : m_ptr{ new int{ *other.m_ptr } }
    {
    }

    // 3. Copy assignment
    OwnedInt& operator=(const OwnedInt& other)
    {
        if (this == &other)
            return *this;

        delete m_ptr;
        m_ptr = new int{ *other.m_ptr };
        return *this;
    }

    // 4. Move constructor
    OwnedInt(OwnedInt&& other) noexcept
        : m_ptr{ other.m_ptr }
    {
        other.m_ptr = nullptr;
    }

    // 5. Move assignment
    OwnedInt& operator=(OwnedInt&& other) noexcept
    {
        if (this == &other)
            return *this;

        delete m_ptr;
        m_ptr = other.m_ptr;
        other.m_ptr = nullptr;
        return *this;
    }

    int get() const
    {
        return *m_ptr;
    }
};

int main()
{
    OwnedInt a{ 10 };
    OwnedInt b{ a };
    OwnedInt c{ std::move(a) };

    b = c;
    c = OwnedInt{ 99 };

    std::cout << b.get() << ' ' << c.get() << '\n';
    return 0;
}

Examine each function. The destructor is still cleaning up the resource owned by the current object; the copy constructor allocates a new int and copies the value pointed to by other.m_ptr; the objects are never sharing space. The copy assignment operator follows the same pattern and then cleans up the resource owned by the current object.

The move constructor steals the pointer that already exists inside other. After the steal, it sets other.m_ptr to nullptr, so the destructor for the source object does not free the address twice. The move assignment operator does the same steal, except the destination first releases whatever it was already pointing to. Both move operations are marked noexcept because they never throw: containers and algorithms depend on that fact.

If any one of these five functions is omitted, the class is broken. Remove the move operations and every temporary is forced down the costly copy path. Remove the copy operations and with the destructor and compiler defined moves we have come full circle, shallow pointer copy again giving us double delete. Specify only the destructor and we are neither safely copyable nor safely movable.

The Rule of Five, then, is not a checklist of additional functions to implement arbitrarily.

 It is the realization that when those five special members are involved, the class has entered into a cohesive agreement concerning ownership. If a class allocates responsibility for a resource, the contract must be fully specified or otherwise the default behavior of the compiler will undermine it silently.

Exercise: The Rule of FiveStarter code, hints and solutionOpenClose
Guided practiceBeginner · 10–15 min

Practice: The Rule of Five

Work through the mission, reveal hints only when needed, and compare your result with the model solution.

Your challenge

Complete OwnedInt with deep-copy construction/assignment and ownership-transferring move construction/assignment, including self-assignment handling. Exercise all four operations without sharing an owning raw pointer or dereferencing an empty source.

Mission checklist

  1. Run the starter as a baseline, then predict the change requested in Your challenge.
  2. Make that change in your editor; use the lesson-specific hints if you need help.
  3. Compile and run your answer, then compare with the worked solution and explain the result.

Starter program

This is the working baseline, not the completed answer. Compile as C++20, then make the change described above.

#include <iostream>

class OwnedInt
{
private:
    int* m_ptr{};

public:
    explicit OwnedInt(int value)
        : m_ptr{ new int{ value } }
    {
    }

    // Run this destructor when the object lifetime ends
    ~OwnedInt()
    {
        delete m_ptr;
    }

    int get() const
    {
        return *m_ptr;
    }
};

int main()
{
    OwnedInt a{ 42 };
    std::cout << a.get() << '\n';
    return 0;
}

Hints

Hint 1

OwnedInt owns m_ptr and deletes it in its destructor, but the starter does not implement the other four special members. Read this as an ownership starting point, not a complete Rule-of-Five class.

Hint 2

Allocate a new int for a copy, but transfer and clear the pointer for a move. In copy assignment, acquire the replacement before deleting the old allocation; handle an empty source and self-assignment.

Worked solution

The completed owner implements all five operations. Copies allocate independent ints; moves transfer the pointer and clear the source. Both assignments handle self-assignment, and get safely handles an empty moved-from owner. The calls exercise copy construction, move construction, copy assignment, and move assignment explicitly.

#include <iostream>
#include <utility>
class OwnedInt
{
    int* m_ptr{};
public:
    explicit OwnedInt(int value) : m_ptr{new int{value}} {}
    ~OwnedInt() { delete m_ptr; }
    OwnedInt(const OwnedInt& other)
        : m_ptr{other.m_ptr ? new int{*other.m_ptr} : nullptr}
    { std::cout << "copy construction\n"; }
    OwnedInt& operator=(const OwnedInt& other)
    {
        if (this != &other) {
            int* copy = other.m_ptr ? new int{*other.m_ptr} : nullptr;
            delete m_ptr;
            m_ptr = copy;
        }
        std::cout << "copy assignment\n";
        return *this;
    }
    OwnedInt(OwnedInt&& other) noexcept : m_ptr{std::exchange(other.m_ptr, nullptr)}
    { std::cout << "move construction\n"; }
    OwnedInt& operator=(OwnedInt&& other) noexcept
    {
        if (this != &other) {
            delete m_ptr;
            m_ptr = std::exchange(other.m_ptr, nullptr);
        }
        std::cout << "move assignment\n";
        return *this;
    }
    bool empty() const { return m_ptr == nullptr; }
    int get() const { return m_ptr ? *m_ptr : 0; }
};
int main()
{
    OwnedInt a{42};
    OwnedInt b{a};
    OwnedInt c{std::move(b)};
    OwnedInt d{0};
    d = a;
    OwnedInt e{0};
    e = std::move(d);
    std::cout << "Values: " << a.get() << ' ' << c.get() << ' ' << e.get() << '\n';
    std::cout << "Moved sources empty: " << b.empty() << ' ' << d.empty() << '\n';
}

Example output

copy construction
move construction
copy assignment
move assignment
Values: 42 42 42
Moved sources empty: 1 1

0 comments

No comments yet. Be the first.

Add a comment

About CppLearn

CppLearn is an interactive, free reference for modern C++. In each lesson, you'll find out how the language works, rather than just lines of code that you can copy and paste. We cover concepts starting from beginner lessons on fundamentals and proceed to templates, move semantics, smart pointers, and areas that usually trap or scare C++ learners.