Skip to content

Rule of Zero, Three and Five Cheatsheet

The five special member functions are where C++ classes acquire lifetime bugs. This sheet is the decision table, the evidence for why it matters, and how to make the whole question disappear.

Pairs with Module 10 — Copying and Resource Management


What the compiler generates, and when

Special member Generated when Suppressed by
Default constructor No constructor is declared at all Declaring any constructor
Destructor Always Nothing
Copy constructor No copy ctor, move ctor, copy assign or move assign is declared Declaring a move operation (then it is deleted, not defaulted)
Copy assignment Same as above Same
Move constructor No copy ctor, copy assign, move assign or destructor is declared Declaring a destructor — this is the one that surprises people
Move assignment Same as above Same

The row that costs marks: declaring a destructor suppresses the implicit move operations, so containers silently copy your objects instead of moving them. Write = default explicitly, or better, own nothing directly.


The three rules

Rule Statement When it applies
Rule of Zero Declare none of the five; own resources through members that already manage them (std::string, std::vector, std::unique_ptr) The goal, and the answer for almost every class you will write
Rule of Three If you write a destructor, copy constructor or copy assignment, write all three A class that owns a resource by hand and must be copyable
Rule of Five The Rule of Three plus the move constructor and move assignment The same class, made efficient
// Rule of Three: correct, but only because it was written deliberately
class Buffer
{
public:
    explicit Buffer(std::size_t n) : data_(new int[n]), size_(n) {}
    ~Buffer() { delete[] data_; }
    Buffer(const Buffer& other) : data_(new int[other.size_]), size_(other.size_)
    {
        std::copy(other.data_, other.data_ + size_, data_);
    }
    Buffer& operator=(const Buffer& other)
    {
        if (this != &other) { Buffer copy(other); swap(copy); }   // copy-and-swap
        return *this;
    }
    void swap(Buffer& other) noexcept
    {
        std::swap(data_, other.data_);
        std::swap(size_, other.size_);
    }
private:
    int* data_;
    std::size_t size_;
};
// Rule of Zero: the same class, with the whole problem removed
class Buffer
{
public:
    explicit Buffer(std::size_t n) : data_(n) {}
    std::size_t size() const { return data_.size(); }
private:
    std::vector<int> data_;
};

No destructor, no copy, no move, no delete, no double free, and the compiler's versions are correct because std::vector already is.


Why this matters: two captured demonstrations

A defaulted copy that double-frees

class Buffer                                  // owns a raw pointer, declares no copy operations
{
public:
    explicit Buffer(int size) : data_(new int[size]), size_(size) {}
    ~Buffer() { delete[] data_; }
private:
    int* data_;
    int size_;
};

int main()
{
    Buffer a(4);
    Buffer b = a;      // compiler-generated copy: both now own one allocation
    std::cout << "two Buffers constructed\n";   // never printed
    return 0;          // both destructors run
}
(no output)

The program aborts inside the heap on the second delete[]. It compiled with -Wall -Wextra -Werror, with no warning at all — this is the class of bug the compiler cannot see, and the reason the Rule of Three exists.

A missing noexcept that costs copies

class Quick { public: Quick() = default; Quick(const Quick&) { ++copies; }
                    Quick(Quick&&) noexcept { ++moves; } /* counts */ };
class Slow  { public: Slow() = default;  Slow(const Slow&) { ++copies; }
                    Slow(Slow&&) { ++moves; } /* no noexcept */ };

Four push_backs of an lvalue into a growing std::vector:

Quick  (move is noexcept): copies=4 moves=3
Slow   (move can throw) : copies=7 moves=0

std::vector will only move during reallocation if the move cannot throw — otherwise it copies, to keep its strong guarantee if a move were to fail. One missing noexcept turned three moves into three copies here, and it turns into a real cost on a large vector.


Deciding what to write

Situation Write
Members are values, std::string, std::vector, smart pointers Nothing — Rule of Zero
Owns a raw resource and should never be copied ~T(), T(const T&) = delete, T& operator=(const T&) = delete
Owns a raw resource, copyable and movable All five (Rule of Five), with noexcept on the moves
Polymorphic base virtual ~T() = default; and nothing else
Move-only handle (a file, a socket, a connection) = delete the copies, = default or write the moves

The details that decide marks

  • std::move does not move. It is a cast to T&&. If the target has no move operation, it copies, silently.
  • Moved-from objects are valid but unspecified. Destroying and reassigning are allowed; assuming it is empty is not.
  • A move constructor should leave the source in a state its destructor can handle. That is the whole contract.
  • Self-assignment must be safe. a = a is legal C++ and a copy-and-swap implementation handles it for free; a naive delete-then-copy does not.
  • Mark moves noexcept unless they genuinely allocate. Check the container behaviour above.
  • Copying a const object cannot move. std::move(const x) yields a const T&&, which binds to const T& — a copy.

Checklist

  • [ ] Does this class own anything directly? If not, did I declare none of the five?
  • [ ] If it owns something, is it a raw pointer? Could a member type own it instead?
  • [ ] Are the move operations noexcept?
  • [ ] Is self-assignment safe?
  • [ ] Does copying a const object still work correctly (it will not move)?
  • [ ] Would a deliberate bug in the copy path be caught by my tests?

Related: Smart Pointers · Constructors and Destructors · Common OOP Bugs · Module 10 — Copying and Resource Management