C++ Rule of Three and Five

The Rule of Three and Rule of Five are guidelines for classes that manage resources such as dynamic memory. They help you decide which special member functions a class should define together.

If a class owns something that must be cleaned up manually, copying and moving that object must also be handled carefully. Otherwise, two objects may accidentally try to manage the same resource.

The Rule of Three

The Rule of Three says that if a class needs any one of these, it probably needs all three:

  • destructor
  • copy constructor
  • copy assignment operator

This usually happens when a class owns a raw resource, such as memory allocated with new. The destructor releases the resource. The copy constructor creates a new object as a copy of another object. The copy assignment operator replaces an existing object’s value with a copy of another object.

The Rule of Five

Modern C++ adds move semantics, so the Rule of Three becomes the Rule of Five. If a class manages a resource and defines custom cleanup or copying, it may also need:

  • move constructor
  • move assignment operator

Move operations transfer ownership of a resource instead of copying it. They are often faster because the new object can take over the existing resource, while the moved-from object is reset to a safe empty state.

Example: A Small Buffer Class

This program defines a class that owns a dynamic array. Because the class uses new[] and delete[], it defines the destructor, copy operations, and move operations explicitly.

#include <iostream>
#include <utility>

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

public:
    Buffer(int count) : data(new int[count]), size(count) {
        for (int i = 0; i < size; i++) {
            data[i] = i + 1;
        }
        std::cout << "created " << size << std::endl;
    }

    ~Buffer() {
        delete[] data;
    }

    Buffer(const Buffer& other) : data(new int[other.size]), size(other.size) {
        for (int i = 0; i < size; i++) {
            data[i] = other.data[i];
        }
        std::cout << "copied" << std::endl;
    }

    Buffer& operator=(const Buffer& other) {
        if (this != &other) {
            int* newData = new int[other.size];
            for (int i = 0; i < other.size; i++) {
                newData[i] = other.data[i];
            }
            delete[] data;
            data = newData;
            size = other.size;
        }
        std::cout << "copy assigned" << std::endl;
        return *this;
    }

    Buffer(Buffer&& other) noexcept : data(other.data), size(other.size) {
        other.data = nullptr;
        other.size = 0;
        std::cout << "moved" << std::endl;
    }

    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            size = other.size;
            other.data = nullptr;
            other.size = 0;
        }
        std::cout << "move assigned" << std::endl;
        return *this;
    }

    int length(void) const {
        return size;
    }

    int first(void) const {
        return data[0];
    }
};

int main(void) {
    Buffer a(3);
    Buffer b = a;

    Buffer c(1);
    c = b;

    Buffer d = std::move(a);

    Buffer e(2);
    e = std::move(c);

    std::cout << "a length: " << a.length() << std::endl;
    std::cout << "b first: " << b.first() << std::endl;
    std::cout << "d length: " << d.length() << std::endl;
    std::cout << "e length: " << e.length() << std::endl;

    return 0;
}

Output:

created 3
copied
created 1
copy assigned
moved
created 2
move assigned
a length: 0
b first: 1
d length: 3
e length: 3

How The Example Works

Buffer a(3) allocates an array. Because Buffer owns that array, its destructor calls delete[] when a Buffer object is destroyed.

Buffer b = a calls the copy constructor. It allocates a separate array for b and copies the values from a. This is a deep copy, so a and b do not share the same pointer.

c = b calls the copy assignment operator. The existing array owned by c is replaced by a new copied array. The check if (this != &other) protects against self-assignment.

Buffer d = std::move(a) calls the move constructor. It transfers a‘s pointer into d, then sets a to nullptr and size 0. This keeps a safe to destroy.

e = std::move(c) calls the move assignment operator. It first deletes e‘s old array, then takes ownership of c‘s array and resets c.

Prefer The Rule Of Zero

The best modern C++ guideline is often the Rule of Zero: design classes so they do not need custom destructors, copy operations, or move operations at all. Standard library types such as std::string, std::vector, and smart pointers already manage their own resources.

When your class stores those types as members, the compiler-generated special member functions are usually correct. Write the Rule of Five by hand mainly when your class directly owns a low-level resource.

Common Mistakes

  • Writing a destructor for a raw pointer but forgetting to define copy behavior.
  • Copying a pointer value instead of copying the resource it points to.
  • Moving a resource without resetting the moved-from object.
  • Reading meaningful data from an object after it has been moved from.

Takeaway: if your class directly owns a manual resource, think in groups: three functions for copying and cleanup, five functions when moving is also supported.