Rule of Three / Five / Zero
Part of the Object Oriented Programming section of Coddy's C++ journey — lesson 25 of 104.
When your class manages resources like dynamic memory, you've learned that you need a custom destructor, copy constructor, and move constructor. But there's a guiding principle that helps you decide which special member functions to implement: the Rule of Three, Five, and Zero.
The Rule of Three states: if you define any of these three, you should define all three:
- Destructor
- Copy constructor
- Copy assignment operator
The Rule of Five extends this for modern C++, adding move operations:
- Destructor
- Copy constructor
- Copy assignment operator
- Move constructor
- Move assignment operator
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t s) : size(s), data(new int[s]) {}
~Buffer() { delete[] data; } // 1. Destructor
Buffer(const Buffer& other); // 2. Copy constructor
Buffer& operator=(const Buffer& other); // 3. Copy assignment
Buffer(Buffer&& other) noexcept; // 4. Move constructor
Buffer& operator=(Buffer&& other) noexcept; // 5. Move assignment
};The Rule of Zero is the simplest approach: if your class doesn't directly manage resources, don't define any of these functions. Let the compiler generate them, or use smart pointers and standard containers that handle resources for you.
class Player {
std::string name; // std::string manages its own memory
std::vector<int> scores; // std::vector handles its resources
public:
Player(std::string n) : name(n) {}
// No destructor, copy, or move functions needed!
};Following these rules prevents bugs like double-deletion, memory leaks, and dangling pointers that occur when some operations are defined but others are missing.
Challenge
EasyLet's build a TextBuffer class that follows the Rule of Five — implementing all five special member functions to properly manage dynamically allocated character data. This will demonstrate how copy and move operations work together to create a robust, resource-managing class.
You'll create two files to organize your code:
TextBuffer.h: Define aTextBufferclass that stores text in a dynamically allocated character array. Your class needs:- Private members: a
char*pointer calleddatafor the text content, and asize_t lengthfor the string length (not including null terminator) - A parameterized constructor that takes a C-string (
const char*), allocates memory, copies the content, and prints"TextBuffer created: <text>" - A destructor that frees memory (if not null) and prints
"TextBuffer destroyed" - A copy constructor that performs a deep copy and prints
"TextBuffer copied" - A copy assignment operator that handles self-assignment, cleans up existing data, performs a deep copy, and prints
"TextBuffer copy-assigned". Return*this - A move constructor (marked
noexcept) that transfers ownership and prints"TextBuffer moved". Leave the source in a valid empty state - A move assignment operator (marked
noexcept) that handles self-assignment, cleans up existing data, transfers ownership, and prints"TextBuffer move-assigned". Return*this - A
getText()method that returns the stored text (return empty string""if data is null) - A
getLength()method that returns the length
- Private members: a
main.cpp: Demonstrate all five special member functions in action. Read a text string from input, then:- Create a
TextBuffercalledoriginalwith the input text - Create
copiedusing the copy constructor fromoriginal - Create
anotherwith the text"Temporary" - Use copy assignment:
another = original - Create
movedby move-constructing fromoriginalusingstd::move() - Create
targetwith the text"Target" - Use move assignment:
target = std::move(copied) - Print
"--- Final State ---" - Print
"original: <text> (length: <len>)"for each buffer: original, copied, moved, another, target
- Create a
After moves, the source objects (original and copied) should show empty text with length 0, while the destination objects hold the transferred data. This demonstrates the Rule of Five in action — all five functions working together to ensure safe resource management.
Include <cstring> for string functions like strlen and strcpy, and <utility> for std::move().
Cheat sheet
The Rule of Three states that if you define any of these three special member functions, you should define all three:
- Destructor
- Copy constructor
- Copy assignment operator
The Rule of Five extends this for modern C++ by adding move operations:
- Destructor
- Copy constructor
- Copy assignment operator
- Move constructor
- Move assignment operator
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t s) : size(s), data(new int[s]) {}
~Buffer() { delete[] data; } // 1. Destructor
Buffer(const Buffer& other); // 2. Copy constructor
Buffer& operator=(const Buffer& other); // 3. Copy assignment
Buffer(Buffer&& other) noexcept; // 4. Move constructor
Buffer& operator=(Buffer&& other) noexcept; // 5. Move assignment
};The Rule of Zero states that if your class doesn't directly manage resources, don't define any special member functions. Use smart pointers and standard containers that handle resources automatically:
class Player {
std::string name; // std::string manages its own memory
std::vector<int> scores; // std::vector handles its resources
public:
Player(std::string n) : name(n) {}
// No destructor, copy, or move functions needed!
};Following these rules prevents bugs like double-deletion, memory leaks, and dangling pointers.
Try it yourself
#include <iostream>
#include <string>
#include <utility>
#include "TextBuffer.h"
using namespace std;
int main() {
string input;
getline(cin, input);
// TODO: Create a TextBuffer called 'original' with the input text
// TODO: Create 'copied' using the copy constructor from 'original'
// TODO: Create 'another' with the text "Temporary"
// TODO: Use copy assignment: another = original
// TODO: Create 'moved' by move-constructing from 'original' using std::move()
// TODO: Create 'target' with the text "Target"
// TODO: Use move assignment: target = std::move(copied)
// TODO: Print "--- Final State ---"
// TODO: Print the state of each buffer in this format:
// "original: <text> (length: <len>)"
// Print for: original, copied, moved, another, target
return 0;
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Fundamentals of OOP
External FilesC++ Build & CompilationHeader Files & Source FilesNamespaces & ScopeIntroduction to OOP in C++Classes vs ObjectsThe 'this' PointerMethods (Member Functions)Attributes (Data Members)Ctors & Dtors BasicsRecap - Simple Calculator4Class Properties
Instance vs Static MembersGetters and SettersConst Member FunctionsMutable KeywordStatic Methods and VariablesFriend Functions & ClassesRecap - Bank Account Manager7Inheritance
Basic InheritanceInheritance Access LevelsCtor & Dtor Call OrderMethod OverridingVirtual Functions & VTableMultiple InheritanceVirtual InheritanceRecap - Employee Hierarchy2Memory Management
Stack vs Heap MemoryPointers and ReferencesDynamic Memory (new/delete)Smart Pointers in C++RAII in C++Recap - Dynamic Array Manager5Encapsulation
Access Specifiers in C++Access Specifiers In DepthInformation HidingStruct vs ClassNested & Inner ClassesRecap - Student Records System8Polymorphism
Compile vs Runtime PolymorphFunction OverloadingVirtual Functions RevisitedPure Virtual FunctionsAbstract ClassesInterface Design in C++Dynamic Casting & RTTIRecap - Shape Calculator3Constructors & Destructors
Default ConstructorParameterized ConstructorCopy ConstructorMove ConstructorConstructor Init ListsDelegating ConstructorsDestructor Deep DiveRule of Three / Five / ZeroRecap - String Class6Operator Overloading
Intro to Operator OverloadArithmetic Operator OverloadComparison Operator OverloadStream OperatorsAssignment Operator Overload[] and () Operator OverloadType Conversion OperatorsRecap - Matrix Class9Templates
Function TemplatesClass TemplatesTemplate SpecializationVariadic TemplatesSFINAE & Type Traits BasicsRecap - Generic Container