Historical Context & Motivation
C++ has always given programmers direct control over memory and system resources, but that power comes with a corresponding responsibility to manage object lifetimes correctly. In the earliest days of the language, developers routinely encountered bugs caused by double deletions, dangling pointers, and resource leaks — all stemming from the compiler's willingness to generate copy operations automatically, even when those default implementations were semantically wrong. The Rule of Three crystallized as a community guideline to address precisely this class of defects, and its later extension to the Rule of Five reflected the new move semantics introduced in C++11.
std::unique_ptr and std::shared_ptr. When that is not possible, the Rule of Five still applies.The fundamental question that the Rule of Three/Five addresses is deceptively simple: if your class manages a resource whose default bitwise copy is incorrect, have you told the compiler how to copy, move, and destroy objects of that type? Failing to answer this question completely leads to some of the most insidious bugs in C++ programs — bugs that may compile cleanly, pass simple tests, and only manifest under specific runtime conditions such as exception unwinding or container reallocation.
Core Principles & Definitions
The Rule of Three/Five is not a language rule enforced by the compiler; it is a design guideline that describes a logical dependency among the special member functions the compiler can generate implicitly. If any one of these functions requires a user-defined implementation — typically because the class directly manages a resource such as heap memory, a file handle, or a network socket — then the default implementations of the others are almost certainly wrong as well. Understanding why requires grasping what the compiler does by default and how resources differ from plain data.
Destructor
delete on raw pointers.Copy Constructor
Copy-Assignment Operator
Move Constructor (C++11)
Move-Assignment Operator (C++11)
The Rule in One Sentence
If you define (or = delete) any one of the destructor, copy constructor, copy-assignment operator, move constructor, or move-assignment operator, you should explicitly define (or = delete / = default) all five. In the pre-C++11 era, the relevant set was only the first three — hence the original Rule of Three. Since C++11 the expanded set gives us the Rule of Five.
Visual Explanation — Shallow vs. Deep Copy
The most common scenario where the Rule of Three/Five matters is a class containing a raw pointer to dynamically allocated memory. The diagram below contrasts what happens under a compiler-generated shallow copy versus a user-defined deep copy. In the shallow-copy case, both objects' internal pointers refer to the same heap block; when one object's destructor frees that block, the other is left with a dangling pointer.
objA and objB pointing to the same heap block, causing a double-free when both destructors run. Right: a user-defined deep copy gives each object its own independent heap allocation.In the shallow-copy scenario on the left, observe how the compiler simply copies the raw pointer value 0xABCD into objB without allocating new memory. When objA's destructor fires and calls delete[] data_, the block at 0xABCD is freed, yet objB still holds a pointer to it. Any subsequent access through objB invokes undefined behavior, and objB's own destructor will attempt to free the same block a second time. The deep-copy version on the right eliminates both problems by allocating a fresh heap block for objB and copying the element values into it.
How It Works — Compiler Generation Rules
Understanding when the compiler generates special member functions — and when it declines to do so — is essential for applying the Rule of Five correctly. The C++ standard specifies a set of interacting conditions that govern implicit generation. Declaring certain functions suppresses the generation of others, sometimes silently, which can produce classes that are copyable but not movable (or vice versa) without the developer realizing it.
Implicit Generation Matrix
| If you declare… | Destructor | Copy Ctor | Copy Assign | Move Ctor | Move Assign |
|---|---|---|---|---|---|
| Nothing | ✓ default | ✓ default | ✓ default | ✓ default | ✓ default |
| Destructor | user | ✓ deprecated† | ✓ deprecated† | ✗ deleted | ✗ deleted |
| Copy Constructor | ✓ default | user | ✓ default | ✗ deleted | ✗ deleted |
| Copy-Assignment Op. | ✓ default | ✓ default | user | ✗ deleted | ✗ deleted |
| Move Constructor | ✓ default | ✗ deleted | ✓ default | user | ✗ deleted |
| Move-Assignment Op. | ✓ default | ✓ default | ✗ deleted | ✗ deleted | user |
The critical observation from this table is that declaring any one of the Big Five has cascading effects on the others. For instance, if you define only a destructor (perhaps to release a resource), the compiler will suppress move operations entirely and continue to generate copy operations only as a deprecated legacy behavior. This means your class will silently fall back to copies where moves could have been used, degrading performance, and the shallow copies remain semantically incorrect. The only safe path is to define all five — or to explicitly = default / = delete them.
Signatures of the Big Five
delete is invoked.*this by reference to enable chaining (e.g., a = b = c). Must handle self-assignment.other and leaves it in a destructible state. Marking noexcept enables optimized container operations (e.g., std::vector reallocation).other, and returns *this. Must handle self-assignment (move from self).Rule of Three vs. Five vs. Zero
Modern C++ recognizes three related guidelines that form a hierarchy of best practice. Choosing the correct rule depends on whether your class directly manages a resource. The diagram below illustrates a decision flowchart that guides you to the appropriate rule for any given class design.
| Guideline | Functions to Define | When to Use |
|---|---|---|
| Rule of Zero | None — rely on compiler defaults | Class members are all RAII types (e.g., std::string, std::vector, std::unique_ptr) |
| Rule of Three | Destructor, copy constructor, copy-assignment operator | Pre-C++11 code or when move semantics are not needed |
| Rule of Five | Destructor, copy ctor, copy assign, move ctor, move assign | C++11+ class that directly manages a resource and cannot delegate to RAII members |
ClassName(const ClassName&) = default; to explicitly request the compiler-generated version, or ClassName(const ClassName&) = delete; to forbid copying entirely. Both of these count as 'declaring' the function for the purposes of the generation rules. Using = default or = delete for all five makes your intent explicit and documents the class's resource semantics at a glance.Worked Example — IntArray Class
Let us build a minimal IntArray class that owns a dynamically allocated array of integers. We will implement all five special member functions following the Rule of Five, walking through each one's responsibilities and explaining the design choices at every step.
int* data_ and std::size_t size_. The parameterized constructor allocates the array:
IntArray(std::size_t n) : data_(new int[n]()), size_(n) {}
The () in new int[n]() value-initializes all elements to zero.n ints.~IntArray() { delete[] data_; }
The destructor frees the heap memory. Because we used new[], we must use delete[]. Defining a destructor triggers the Rule of Five — we must now address all remaining special member functions.IntArray(const IntArray& other) : data_(new int[other.size_]), size_(other.size_) { std::copy(other.data_, other.data_ + size_, data_); }
We allocate a new block of the same size and copy element values using std::copy. This guarantees the copy has its own independent storage.IntArray& operator=(IntArray other) { swap(*this, other); return *this; }
By accepting other by value, the copy (or move) constructor is invoked to create other. We then swap data_ and size_ between *this and other. When other goes out of scope, its destructor frees our old memory. This idiom is self-assignment safe and exception safe.IntArray(IntArray&& other) noexcept : data_(other.data_), size_(other.size_) { other.data_ = nullptr; other.size_ = 0; }
We steal other's pointer and size, then null out other.data_ so that other's destructor does not free our newly acquired memory. Marking noexcept is critical — std::vector will only use move during reallocation if the move constructor is noexcept.other parameter is move-constructed rather than copy-constructed, giving us move-assignment semantics for free. Alternatively, one could write a separate operator=(IntArray&& other) noexcept that performs delete[] data_; data_ = other.data_; size_ = other.size_; other.data_ = nullptr; other.size_ = 0; return *this; for maximum efficiency.swap function: friend void swap(IntArray& a, IntArray& b) noexcept { using std::swap; swap(a.data_, b.data_); swap(a.size_, b.size_); }. This enables argument-dependent lookup (ADL) and ensures swapping is noexcept.Strengths, Pitfalls, and Common Mistakes
Applying the Rule of Five correctly prevents an entire category of bugs, but it also introduces complexity. Understanding the trade-offs helps you decide when to implement the rule directly versus when to refactor toward the Rule of Zero.
| Strengths | Pitfalls / Limitations |
|---|---|
| Eliminates double-free and use-after-free bugs by ensuring every copy is deep and every move leaves the source safe. | Five functions to write, test, and maintain per resource-owning class — a significant maintenance burden. |
| Move semantics (Rule of Five) enable O(1) resource transfer, dramatically improving performance for objects stored in STL containers. | Forgetting noexcept on move operations silently degrades container performance — std::vector falls back to copying. |
| The copy-and-swap idiom provides a single, exception-safe implementation pattern that handles both copy and move assignment. | Copy-and-swap always allocates a temporary, which can be suboptimal in tight loops compared to a hand-written assignment operator. |
| Makes the class's resource semantics explicit and documented in the interface. | If a derived class also manages resources, the base class's Big Five alone are insufficient — the derived class needs its own set. |
Common Mistakes
- Defining only the destructor: The compiler still generates (deprecated) shallow copy operations and suppresses moves. The class appears to work until objects are copied.
- Not nulling the source pointer in the move constructor: The moved-from object's destructor will free the memory now owned by the move target, causing a double-free.
- Ignoring self-assignment in the assignment operator: Writing
delete[] data_; data_ = new int[other.size_]; ...destroysdata_before reading from it when&other == this. The copy-and-swap idiom avoids this pitfall entirely. - Using the wrong form of delete: Allocating with
new[]but freeing withdelete(instead ofdelete[]) is undefined behavior.
std::unique_ptr) and compose your class from those wrappers. Then the compiler-generated defaults are correct, and you write zero special member functions. Reserve the Rule of Five for low-level resource handles where smart pointers are insufficient.Connection to Advanced Topics
The Rule of Five does not exist in isolation; it connects deeply to several advanced C++ mechanisms. Understanding these connections prepares you for more sophisticated resource management patterns and template-heavy library code.
| This Lesson | Advanced Extension |
|---|---|
| Copy constructor / copy-assignment operator | Copy-and-Swap idiom — a canonical pattern providing strong exception safety and DRY implementation of both copy and move assignment. |
| Move constructor / move-assignment operator | Rvalue references and perfect forwarding — std::forward and universal references enable generic code to preserve value categories. |
| Rule of Zero | RAII and smart pointers — std::unique_ptr (exclusive ownership), std::shared_ptr (shared ownership), custom deleters. |
| Suppression of implicit generation | = default / = delete — explicit control over special members, non-copyable/non-movable types, constrained APIs. |
| Resource-owning base class | Virtual destructors and polymorphic ownership — if you delete through a base pointer, the destructor must be virtual; this interacts with Rule of Five generation rules. |
As you move into template metaprogramming and library design, you will encounter type traits such as std::is_copy_constructible, std::is_nothrow_move_constructible, and std::is_trivially_destructible. These compile-time queries inspect the special member functions you have (or have not) defined, and generic algorithms and containers use them to select optimal code paths. A class that incorrectly omits noexcept on its move constructor, for example, will cause std::is_nothrow_move_constructible<T>::value to be false, forcing std::vector<T> to copy rather than move during reallocation — a subtle performance cliff that traces directly back to how you apply the Rule of Five.
Practice Problems
FileLogger has a user-defined destructor that closes a file handle, but no other special member functions are declared. Under C++17, which of the Big Five does the compiler generate implicitly, and what is the potential hazard?Buffer with member char* data_ allocated via new char[capacity_]. Write the signature and body of its copy constructor so that it performs a deep copy.class Matrix { int* data_; size_t rows_, cols_; public: Matrix(size_t r, size_t c) : data_(new int[r*c]()), rows_(r), cols_(c) {} ~Matrix() { delete[] data_; } };UniqueSocket class that wraps a Unix file descriptor (an int returned by socket()). The socket should be closed with close(fd) on destruction, ownership should be transferable (movable) but not copyable. Which of the Big Five do you define, which do you = delete, and what does the move constructor body look like?Summary — The Rule of Three/Five at a Glance
The Rule of Three states that if a class requires a user-defined destructor, copy constructor, or copy-assignment operator, it almost certainly requires all three. In C++11 and later, the Rule of Five extends this to include the move constructor and move-assignment operator. The root cause is that compiler-generated defaults perform shallow (member-wise) operations that are incorrect when the class manages a resource through a raw handle. Violating the rule leads to double-free, use-after-free, dangling pointers, and resource leaks.
Modern C++ best practice favors the Rule of Zero: compose classes from RAII members like std::unique_ptr and std::vector so that the compiler-generated special member functions are correct by default. When direct resource management is unavoidable, apply the Rule of Five and consider the copy-and-swap idiom for exception-safe assignment. Always mark move operations noexcept to unlock optimal STL container behavior, and use = default or = delete to make your intent explicit for every special member function.