C++ • C++-SPECIFIC TOPICS

Rule of Three/Five — Understand the Rule of Three/Five conceptually

Master the essential idiom that prevents resource leaks and undefined behavior in C++ classes.

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.

1979–1985
C with Classes → C++
Bjarne Stroustrup develops C++ with constructors and destructors, establishing the RAII (Resource Acquisition Is Initialization) idiom. The compiler begins generating default copy constructors and copy-assignment operators for every class.
1998
C++98 Standardized
The first ISO standard formalizes the implicit generation rules for the destructor, copy constructor, and copy-assignment operator. Community experts begin articulating the Rule of Three as an essential guideline in books and mailing lists.
2000–2005
Rule of Three Codified
Authors such as Scott Meyers (Effective C++) and Herb Sutter (Exceptional C++) popularize the rule. It becomes a standard interview question and a core element of C++ pedagogy.
2011
C++11 and Move Semantics
C++11 introduces rvalue references, the move constructor, and the move-assignment operator. The Rule of Three expands to the Rule of Five to account for these two new special member functions.
2011–Present
Rule of Zero Emerges
Modern best practice advocates the Rule of Zero: design classes so that compiler-generated defaults are correct by composing RAII wrappers like 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.

1

Destructor

Invoked when an object's lifetime ends. Responsible for releasing resources. The compiler-generated destructor calls destructors of all members but does not call delete on raw pointers.
2

Copy Constructor

Creates a new object as a copy of an existing one. The compiler-generated version performs a member-wise (shallow) copy, which copies pointer values rather than the memory they point to.
3

Copy-Assignment Operator

Replaces the contents of an existing object with a copy of another. Must handle self-assignment safely and release any previously held resources before acquiring new ones.
4

Move Constructor (C++11)

Transfers ownership of resources from a temporary (rvalue) into a new object, leaving the source in a valid but unspecified state. Avoids expensive deep copies.
5

Move-Assignment Operator (C++11)

Transfers resources from a temporary into an existing object. Combines the concerns of copy-assignment (release old resources) and move construction (steal new resources).

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.

✦ KEY TAKEAWAY
Think of a class that manages a resource as a person holding a lease on an apartment. The destructor is what happens when the lease ends — you vacate and return the keys. The copy constructor is like signing a second lease for a friend on a separate apartment with identical furniture, not handing them a duplicate key to your apartment. If you correctly handle vacating (destructor) but forget to handle the lease-duplication scenario (copy constructor), two people end up sharing a single apartment unknowingly — and when one moves out, the other's belongings are thrown away. The Rule of Three/Five says: if you need custom logic for any of these lease operations, you almost certainly need it for all of them.

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.

Left: the compiler-generated shallow copy leaves both 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

† Generating copy operations when a destructor is user-declared is deprecated in C++11 and later.
If you declare…DestructorCopy CtorCopy AssignMove CtorMove Assign
Nothing✓ default✓ default✓ default✓ default✓ default
Destructoruser✓ deprecated†✓ deprecated†✗ deleted✗ deleted
Copy Constructor✓ defaultuser✓ default✗ deleted✗ deleted
Copy-Assignment Op.✓ default✓ defaultuser✗ deleted✗ deleted
Move Constructor✓ default✗ deleted✓ defaultuser✗ deleted
Move-Assignment Op.✓ default✓ default✗ deleted✗ deleteduser

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

DESTRUCTOR
~ClassName();
No parameters, no return type. Called automatically when scope ends or delete is invoked.
COPY CONSTRUCTOR
ClassName(const ClassName& other);
Takes a const lvalue reference. Invoked during initialization from an existing object, pass-by-value, or return-by-value (when copy elision does not apply).
COPY-ASSIGNMENT OPERATOR
ClassName& operator=(const ClassName& other);
Returns *this by reference to enable chaining (e.g., a = b = c). Must handle self-assignment.
MOVE CONSTRUCTOR
ClassName(ClassName&& other) noexcept;
Takes an rvalue reference. Steals resources from other and leaves it in a destructible state. Marking noexcept enables optimized container operations (e.g., std::vector reallocation).
MOVE-ASSIGNMENT OPERATOR
ClassName& operator=(ClassName&& other) noexcept;
Releases current resources, steals from 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.

A decision flowchart guiding class designers from the preferred Rule of Zero through the Rule of Three (pre-C++11) to the Rule of Five (C++11 and later). The key branching point is whether the class directly manages a resource that cannot be wrapped in an RAII smart pointer.
Comparison of the three related guidelines
GuidelineFunctions to DefineWhen to Use
Rule of ZeroNone — rely on compiler defaultsClass members are all RAII types (e.g., std::string, std::vector, std::unique_ptr)
Rule of ThreeDestructor, copy constructor, copy-assignment operatorPre-C++11 code or when move semantics are not needed
Rule of FiveDestructor, copy ctor, copy assign, move ctor, move assignC++11+ class that directly manages a resource and cannot delegate to RAII members
💡 Tip: = default and = delete
In C++11 and later you can write 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.

IntArray: Full Rule-of-Five Implementation
1
Step 1 — Define the Class and ConstructorWe declare private members 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.
Class owns a heap-allocated array of n ints.
2
Step 2 — Destructor~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.
Heap memory is correctly released when the object goes out of scope.
3
Step 3 — Copy Constructor (Deep Copy)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.
Two independent objects with identical contents.
4
Step 4 — Copy-Assignment Operator (Copy-and-Swap Idiom)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.
Strong exception guarantee; old resources freed via destructor of temporary.
5
Step 5 — Move ConstructorIntArray(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.
O(1) resource transfer; source left in a valid empty state.
6
Step 6 — Move-Assignment OperatorBecause we used the copy-and-swap idiom in Step 4, the copy-assignment operator already handles move assignment! When the right-hand side is an rvalue, the 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.
Copy-and-swap unifies both assignment operators; a dedicated overload avoids one extra move.
🔀 The swap Function
For the copy-and-swap idiom to work, provide a friend 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 vs. pitfalls of applying the Rule of Five
StrengthsPitfalls / 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_]; ... destroys data_ 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 with delete (instead of delete[]) is undefined behavior.
⚙ DESIGN PREFERENCE
The Rule of Five is a necessary safeguard, but it is also a code smell: it signals that your class is directly managing a raw resource. In modern C++, the preferred approach is the Rule of Zero — encapsulate each resource in a dedicated RAII wrapper (such as 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.

How the Rule of Five connects to advanced C++ topics
This LessonAdvanced Extension
Copy constructor / copy-assignment operatorCopy-and-Swap idiom — a canonical pattern providing strong exception safety and DRY implementation of both copy and move assignment.
Move constructor / move-assignment operatorRvalue references and perfect forwarding — std::forward and universal references enable generic code to preserve value categories.
Rule of ZeroRAII 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 classVirtual 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

PROBLEM 1 — CONCEPTUAL
A class 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?
PROBLEM 2 — BASIC CALCULATION
Consider a class 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.
PROBLEM 3 — INTERMEDIATE
You have the following class. Identify all Rule-of-Five violations and explain what could go wrong: 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_; } };
PROBLEM 4 — APPLIED
You are writing a 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?
PROBLEM 5 — CRITICAL THINKING
A colleague argues: "The Rule of Five is obsolete because we have smart pointers now. You should never need to write a destructor." Provide a nuanced response that identifies at least two scenarios where the Rule of Five remains necessary even in modern C++, and explain why the Rule of Zero does not fully replace it.

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.

Varsity Tutors • C++ • Rule of Three/Five — Understand the Rule of Three/Five conceptually