Rule of Three/Five — C++
From Rule of Three to Rule of Five
The Rule of Three (C++98) says: if a class defines any one of these, it must define all three:
- Destructor
- Copy constructor
- Copy assignment operator
Modern C++ (C++11 and later) adds move semantics, extending this to the Rule of Five:
- Destructor
- Copy constructor
- Copy assignment operator
- Move constructor (C++11)
- Move assignment operator (C++11)
[!NOTE] See also: Rule of Three (video) and Move Semantics (video)
flowchart TD
A["Class manages dynamic resource"]
B["Destructor"]
C["Copy constructor"]
D["Copy assignment operator"]
E["Move constructor (C++11)"]
F["Move assignment operator (C++11)"]
G["All 5 must handle memory correctly"]
A --> B
A --> C
A --> D
A --> E
A --> F
B --> G
C --> G
D --> G
E --> G
F --> G
Why It Matters
These member functions initialize and manage an object’s owned resources.
Improper handling leads to:
- 🔴 Double deletion
- 🔴 Memory leaks
- 🔴 Dangling pointers
- 🔴 Unnecessary copies (performance)
flowchart LR
X["Constructor allocates (new)"] --> Y["Destructor deallocates (delete)"]
Y --> Z["Copy: duplicate safely"]
Z --> M["Move: transfer ownership (no copy)"]
M --> X
Example Class: MaybeInt
A class that may or may not own an integer in dynamic memory.
class MaybeInt {
public:
MaybeInt() : value_(nullptr) {}
MaybeInt(int value) : value_(new int(value)) {}
// 1. Destructor
~MaybeInt() { delete value_; }
// 2. Copy constructor — deep copy
MaybeInt(const MaybeInt& other) {
value_ = other.value_ ? new int(*other.value_) : nullptr;
}
// 3. Copy assignment operator — deep copy
MaybeInt& operator=(const MaybeInt& other) {
if (this != &other) {
delete value_;
value_ = other.value_ ? new int(*other.value_) : nullptr;
}
return *this;
}
// 4. Move constructor — transfer ownership
MaybeInt(MaybeInt&& other) noexcept : value_(other.value_) {
other.value_ = nullptr; // leave source in valid empty state
}
// 5. Move assignment operator — transfer ownership
MaybeInt& operator=(MaybeInt&& other) noexcept {
if (this != &other) {
delete value_;
value_ = other.value_;
other.value_ = nullptr; // leave source in valid empty state
}
return *this;
}
private:
int* value_;
};
classDiagram
class MaybeInt {
- int* value_
+ MaybeInt()
+ MaybeInt(int value)
+ ~MaybeInt()
+ MaybeInt(MaybeInt const-ref other)
+ operator=(MaybeInt const-ref other) MaybeInt-ref
+ MaybeInt(MaybeInt rref other)
+ operator=(MaybeInt rref other) MaybeInt-ref
}
Bug Example — Shallow Copy (No Rule of Three/Five)
int main() {
MaybeInt a(7);
MaybeInt b(a); // Copy constructor (shallow without Rule of Five)
MaybeInt c;
c = a; // Copy assignment (shallow without Rule of Five)
return 0; // Both a and b delete the same pointer!
}
🔗 Interactive Visualization
👉 View shallow-copy bug in Python Tutor
Animation — Copy Problem (Shallow Copy / Double Delete)
Move Semantics — What and Why
Copying duplicates data. Moving transfers ownership — the source is left in a valid but empty state. This is especially important for performance when returning objects from functions or inserting into containers.
MaybeInt a(42);
MaybeInt b = std::move(a); // Move constructor: b now owns the int, a.value_ == nullptr
flowchart LR
A["a::value_ points to heap(42)"] -->|"std::move(a)"| B["Move constructor called"]
B --> C["b::value_ now owns heap(42)"]
B --> D["a::value_ set to nullptr"]
Copy vs. Move Comparison
| Operation | Source after | Memory allocated | Performance |
|---|---|---|---|
| Copy constructor / assignment | Unchanged (still owns data) | New allocation (new) |
Slower (deep copy) |
| Move constructor / assignment | Emptied (nullptr) |
No allocation (pointer transfer) | Fast (pointer swap) |
Animation — Move Semantics
Interactive Python Tutor — All Five Functions
Click the link below to step through all five special member functions in Python Tutor:
👉 Open in Python Tutor (all 5 functions)
Summary Table — The Five Special Member Functions
| Function | Syntax | Purpose | Must Handle |
|---|---|---|---|
| Destructor | ~T() |
Destroys object, frees resources | delete |
| Copy constructor | T(const T&) |
Creates a deep copy | new |
| Copy assignment | T& operator=(const T&) |
Replaces content with deep copy | delete + new |
| Move constructor (C++11) | T(T&&) noexcept |
Transfers ownership from rvalue | Steal pointer, null source |
| Move assignment (C++11) | T& operator=(T&&) noexcept |
Replaces content by ownership transfer | delete old, steal, null source |
[!TIP] Rule of Zero: If you use smart pointers (
std::unique_ptr,std::shared_ptr) or standard containers, you can often avoid writing any of these five functions yourself - the compiler-generated defaults will do the right thing.
Rule of Zero — Prefer Smart Pointers
#include <memory>
class MaybeInt {
public:
MaybeInt() = default;
explicit MaybeInt(int v) : value_(std::make_unique<int>(v)) {}
// No destructor, no copy/move — compiler-generated defaults are correct!
private:
std::unique_ptr<int> value_;
};
[!NOTE] In modern C++, if you define any of the five special member functions, define all five - or use smart pointers (Rule of Zero) and define none.