Skip to content
The Rule of Five and RAII
step 1/6

Reading — step 1 of 6

Learn

~2 min readModern C++ Patterns

When a class manages a resource (raw pointer, file handle, mutex, etc.), the compiler auto-generates copy/move constructors and destructors. The defaults are usually wrong for resource-managing classes — leading to double-frees, leaks, and dangling pointers.

The Rule of Five says: if you define ANY of these five, you usually need to define ALL of them.

The five special member functions

class Buffer {
public:
    Buffer(size_t n) : data_(new int[n]), size_(n) {}
    
    // 1. Destructor
    ~Buffer() { delete[] data_; }
    
    // 2. Copy constructor
    Buffer(const Buffer& other) : data_(new int[other.size_]), size_(other.size_) {
        std::copy(other.data_, other.data_ + size_, data_);
    }
    
    // 3. Copy assignment
    Buffer& operator=(const Buffer& other) {
        if (this == &other) return *this;
        delete[] data_;
        size_ = other.size_;
        data_ = new int[size_];
        std::copy(other.data_, other.data_ + size_, data_);
        return *this;
    }
    
    // 4. Move constructor
    Buffer(Buffer&& other) noexcept : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;
        other.size_ = 0;
    }
    
    // 5. Move assignment
    Buffer& operator=(Buffer&& other) noexcept {
        if (this == &other) return *this;
        delete[] data_;
        data_ = other.data_;
        size_ = other.size_;
        other.data_ = nullptr;
        other.size_ = 0;
        return *this;
    }
    
private:
    int* data_;
    size_t size_;
};

The Rule of Zero

If you can avoid managing resources directly — by using smart pointers, STL containers, etc. — you don't need ANY of the five. The compiler-generated defaults work correctly for classes whose members are themselves well-behaved.

class GoodBuffer {
public:
    GoodBuffer(size_t n) : data_(n) {}
    // No special functions needed — std::vector handles everything
private:
    std::vector<int> data_;
};

Rule of Zero is the modern default. Reach for Rule of Five only when you absolutely have to manage a raw resource.

Why each function matters

Without proper copy ctor / copy assignment, copying a Buffer copies the POINTER — both objects now own the same array. When the first goes out of scope and deletes, the second is dangling. Crash.

Without proper move ctor / move assignment, returning a Buffer by value forces a copy when it could have been a cheap move (just transfer the pointer).

= default and = delete

You can explicitly opt in or out:

class NonCopyable {
public:
    NonCopyable() = default;
    NonCopyable(const NonCopyable&) = delete;             // can't copy
    NonCopyable& operator=(const NonCopyable&) = delete;  // can't assign
    NonCopyable(NonCopyable&&) = default;                  // but can move
    NonCopyable& operator=(NonCopyable&&) = default;
};

Classic use: file handles, mutexes, sockets — things where copying makes no sense.

Common mistakes

  • Defining only the destructor — copy still uses the default (shallow). Either delete the copy or write it.
  • Forgetting noexcept on move ops — STL containers may fall back to copying for exception safety.
  • Self-assignment in copy assignment — guard with if (this == &other) return *this;.
  • Manually managing memory when std::vector / std::string / smart_ptr would do — you're writing the Rule of Five for nothing.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…