Rule of 3 / 5 / 0 - copy, move, destructor
Scenario
You write a class that holds a raw pointer, copy one object into another, and both destructors run at scope end. Your program crashes with 'double free'.
You never called delete twice — so who did?
Mental model
The compiler silently writes a copy-constructor for you, and its copy is SHALLOW: it duplicates the pointer, not what it points to. Now two objects own the same memory — and both will free it.
If your class manages a raw resource (heap memory, a file handle, a socket), the compiler-generated copy operations do the wrong thing. The Rule of 3 says: if you need a custom destructor, you almost certainly need a custom copy-constructor and copy-assignment too. C++11 adds move-constructor and move-assignment (Rule of 5). The Rule of 0 is the escape hatch: own nothing raw (use std::vector / std::unique_ptr) and you need to write NONE of them.
Explanation
A class that owns a raw pointer is a trap, because C++ gives every class a free copy-constructor and copy-assignment operator — and they copy member-by-member. For a pointer member that means copying the ADDRESS, so the original and the copy point at the same heap block. When both objects go out of scope, both destructors call delete on that one block: undefined behaviour, usually a crash.
The Rule of 3 (pre-C++11): if you define any one of destructor, copy-constructor, or copy-assignment, you need all three, because their presence signals you're managing a resource the defaults can't handle.
C++11 adds move operations, making it the Rule of 5: destructor, copy-ctor, copy-assign, move-ctor, move-assign. A move transfers ownership (steal the pointer, null out the source) instead of duplicating — cheap, and correct for temporaries.
The modern answer is the Rule of 0: don't manage raw resources at all. Hold a std::vector or std::unique_ptr, whose own copy/move semantics are already correct, and let the compiler-generated special members just work. Reach for Rule of 5 only when you're writing the resource-owning wrapper itself.
When writing a move-constructor, you should almost always mark it `noexcept`. Standard library containers like `std::vector` will only use your move-constructor during reallocation if they are guaranteed it won't throw exceptions; otherwise, they fall back to the slower copy-constructor.
Code examples
The bug: shallow copy → double free
struct Buf {
int* data;
Buf(int n) : data(new int[n]) {}
~Buf() { delete[] data; } // custom dtor, but NO custom copy
};
int main() {
Buf a(10);
Buf b = a; // shallow copy: b.data == a.data
} // both dtors run -> delete[] the SAME pointer twice -> crash
Buf defines a destructor but relies on the compiler's shallow copy-constructor. `b = a` copies the pointer value, so a.data and b.data alias the same block. At the closing brace both are destroyed and delete[] is called twice on one address — a double free.
The fix: Rule of 5 (deep copy + move)
struct Buf {
int* data; int n;
Buf(int n) : data(new int[n]), n(n) {}
~Buf() { delete[] data; }
Buf(const Buf& o) : data(new int[o.n]), n(o.n) { // copy: allocate + duplicate
std::copy(o.data, o.data + o.n, data);
}
Buf& operator=(Buf o) { std::swap(data, o.data); std::swap(n, o.n); return *this; } // copy-and-swap
Buf(Buf&& o) noexcept : data(o.data), n(o.n) { o.data = nullptr; o.n = 0; } // move: steal
};
The copy-constructor allocates its OWN block and duplicates the contents (deep copy), so each object frees a distinct pointer. The move-constructor steals the pointer and nulls the source, so the moved-from object's delete[] is a harmless delete[] nullptr. The copy-and-swap assignment handles both cleanly and is self-assignment-safe.
The best fix: Rule of 0
struct Buf {
std::vector<int> data; // owns nothing raw
Buf(int n) : data(n) {}
// no dtor, no copy, no move — the compiler's defaults are already correct
};
By delegating ownership to std::vector (whose copy/move are correct), Buf needs none of the five special members. This is the idiom to reach for by default — write the Rule of 5 only inside the resource wrapper itself.
Key points
- The tell: A custom destructor with the default (shallow) copy still enabled = latent double-free.
- Rule of 5: dtor + copy-ctor + copy-assign + move-ctor + move-assign, as a set.
- Default to Rule of 0: Own resources through std::vector/unique_ptr; write the five only in the wrapper itself.
Common mistakes
- Defining a destructor but not the copy operations: The most common source of double-free: a custom ~T() with the compiler's shallow copy still active. Either define all of Rule of 3/5, or =delete the copies, or use Rule of 0.
- Forgetting noexcept on the move constructor: std::vector only uses your move-ctor during reallocation if it's noexcept; otherwise it falls back to copying for exception safety. A non-noexcept move silently pessimizes performance.
Recall questions
- Why does a class with a raw owning pointer and only a custom destructor crash on copy?
- State the Rule of 3 and what C++11 added to make it the Rule of 5.
- What is the Rule of 0 and why prefer it?
Questions & answers
A candidate's String class double-frees on copy. What's missing and how do you fix it three ways?
It has a destructor but relies on the shallow default copy. Fix: (1) define a deep copy-ctor + copy-assign (Rule of 3), (2) add move ops (Rule of 5), or (3) replace the raw char* with std::string (Rule of 0).
Approach: Recognize the destructor-without-copy signature as the tell.
A candidate implements a move constructor for their class but forgets the `noexcept` keyword. Why might this cause a performance drop when using `std::vector`?
When `std::vector` resizes, it needs to move elements to a new memory block. If the move constructor is not marked `noexcept`, `std::vector` will fall back to using the copy constructor to ensure strong exception safety (so it doesn't lose data if an exception is thrown mid-resize). This silently pessimizes performance.
Approach: Understand the interaction between move semantics, exception safety, and standard library containers.
Continue learning
Previous: Classes, constructors, destructors & this