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:

  1. Destructor
  2. Copy constructor
  3. Copy assignment operator

Modern C++ (C++11 and later) adds move semantics, extending this to the Rule of Five:

  1. Destructor
  2. Copy constructor
  3. Copy assignment operator
  4. Move constructor (C++11)
  5. 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:

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)

Open in Python Tutor


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.