Skip to content
C++ Better Explained
Go back
The Rule of Three in C++ Explained (with Examples)

The Rule of Three in C++ Explained (with Examples)

The Rule of Three is a guideline that saves beginners from one of C++‘s nastiest bugs: crashes and memory corruption when objects are copied. It states that if your class needs a custom destructor, copy constructor, or copy assignment operator, it almost certainly needs all three. Let’s see exactly why.


The Three Special Functions

When a class manages a resource — most commonly memory allocated with new — three functions control its lifetime:

  1. Destructor — cleans up when the object dies (~MyClass())
  2. Copy constructor — builds a new object as a copy of an existing one (MyClass(const MyClass&))
  3. Copy assignment operator — replaces one existing object’s contents with another’s (operator=)

If you don’t write these, the compiler generates them for you. For simple classes that’s fine. But when your class owns a raw pointer, the auto-generated versions do the wrong thing.


The Bug: A Class That Owns Memory

Here’s a class that allocates memory but only defines a destructor:

#include <iostream>
#include <cstring>
using namespace std;

class Buffer {
    char* data;
public:
    Buffer(const char* text) {
        data = new char[strlen(text) + 1];
        strcpy(data, text);
    }
    ~Buffer() {
        delete[] data;   // free the memory
    }
    void print() const { cout << data << "\n"; }
};

int main() {
    Buffer a("hello");
    Buffer b = a;        // compiler-generated copy — DANGER
    return 0;            // both destructors run...
}

This program crashes. Why? The compiler’s default copy just copies the pointer, so a.data and b.data point at the same memory. When both objects are destroyed, delete[] runs twice on that one block — a “double free” that corrupts the heap.

This is called a shallow copy. What we need is a deep copy: each object gets its own independent memory.


The Fix: Implement All Three

#include <iostream>
#include <cstring>
using namespace std;

class Buffer {
    char* data;
public:
    // Constructor
    Buffer(const char* text) {
        data = new char[strlen(text) + 1];
        strcpy(data, text);
    }

    // 1. Destructor
    ~Buffer() {
        delete[] data;
    }

    // 2. Copy constructor — allocate our OWN copy
    Buffer(const Buffer& other) {
        data = new char[strlen(other.data) + 1];
        strcpy(data, other.data);
    }

    // 3. Copy assignment operator
    Buffer& operator=(const Buffer& other) {
        if (this == &other) return *this;   // guard against self-assignment
        delete[] data;                      // free our old memory
        data = new char[strlen(other.data) + 1];
        strcpy(data, other.data);
        return *this;
    }

    void print() const { cout << data << "\n"; }
};

int main() {
    Buffer a("hello");
    Buffer b = a;        // deep copy — safe now
    b.print();
    return 0;            // each frees its own memory — no crash
}

Now a and b each own separate memory, so both destructors run safely. This is why the rule bundles the three together: they all deal with the same underlying question — how is this resource copied and released? Fixing one without the others leaves a hole.

If you're looking to go deeper with C++, the C++ Better Explained Ebook is perfect for you — whether you're a complete beginner or looking to solidify your understanding. Just $19.

Why “If One, Then All Three”

The logic is straightforward. If your class needs a custom destructor, it owns a resource that must be released manually. But a resource that must be released manually must also be copied manually — otherwise the default shallow copy shares it, and you’re back to the double-free bug. So needing any one of the three strongly implies needing the other two.


The Modern Answer: Rule of Zero

Here’s the twist experienced C++ programmers will tell you: the best way to follow the Rule of Three is to not need it at all.

If you store your data in types that already manage their own memory — like std::string or std::vector — the compiler-generated copy and destructor already do the right thing. You write zero special functions:

#include <string>
using namespace std;

class Buffer {
    string data;          // string handles its own memory
public:
    Buffer(const string& text) : data(text) {}
    // No destructor, copy constructor, or operator= needed!
};

This is called the Rule of Zero: prefer classes that need none of the three because their members clean up after themselves. Use the Rule of Three only when you truly must manage a raw resource yourself.


Rule of Three vs Rule of Five

C++11 added move semantics, which introduced two more special functions: the move constructor and move assignment operator. When you extend the Rule of Three with these, it becomes the Rule of Five. The idea is the same — if you manage a resource by hand, define all the functions that copy, move, and release it so nothing is left to a broken default.


Quick Summary

RuleFunctions you defineWhen
Rule of Zerononeyour members manage their own memory (preferred)
Rule of Threedestructor + copy constructor + copy assignmentyou manage a raw resource
Rule of Fivethe three above + move constructor + move assignmentyou manage a resource and want efficient moves

The Takeaway

If you find yourself writing a destructor because your class owns something raw, stop and write the copy constructor and copy assignment operator too. Better yet, lean on std::string, std::vector, and smart pointers so you never have to — that’s the Rule of Zero, and it’s the safest path of all.



Take Your C++ Further

If you’re looking to go deeper with C++, the C++ Better Explained Ebook is perfect for you — whether you’re a complete beginner or looking to solidify your understanding. Just $19.

👉 Get the C++ Better Explained Ebook — $19

📋

Free Download: The 10 Mistakes Every C++ Beginner Makes

A free 1-page checklist that shows the exact traps that slow down every C++ beginner — so you can avoid them from day one.

🔒 No spam. Unsubscribe anytime.


Share this post on:

Written by

Sahil Bora

Software Engineer. Author and creator of C++ Better Explained.


Previous Post
C++ priority_queue: A Beginner's Guide with Examples
Next Post
What Is size_t in C++? The Type for Sizes and Indexing

Keep Learning