The Rule of Five in C++ is an extension of the Rule of Three. It acknowledges the introduction of move semantics in C++11 and states that if a class requires any of the following five special member functions, it should likely implement all five:
- Destructor
- Copy Constructor
- Copy Assignment Operator
- Move Constructor (new in C++11)
- Move Assignment Operator (new in C++11)
Why the Rule of Five?
In C++11 and later, move semantics allow objects to transfer ownership of resources instead of duplicating them. This is particularly useful for classes that manage resources like dynamic memory, file handles, or sockets, as it can significantly improve performance by avoiding expensive deep copies.
The Five Special Member Functions:
1. Destructor
The destructor cleans up resources when an object is destroyed.
2. Copy Constructor
The copy constructor creates a new object as a deep copy of another.
3. Copy Assignment Operator
The copy assignment operator assigns one object's resources to another, making a deep copy.
4. Move Constructor
The move constructor transfers resources from one object to another, leaving the original object in a valid but empty state. This avoids expensive deep copying.
5. Move Assignment Operator
The move assignment operator transfers resources from one object to another, cleaning up any existing resources in the target object.
The Problem Without the Rule of Five
When a class manages resources, such as dynamic memory or file handles, and does not implement the special member functions (destructor, copy/move constructors, copy/move assignment operators), the compiler automatically generates default implementations for these functions. However, these default implementations often perform a shallow copy or a naive move, which can lead to serious problems:
1 Shallow Copy Issue
By default, the compiler-generated copy constructor and copy assignment operator perform a shallow copy of all member variables. This is problematic when the class manages resources like dynamic memory.
Example:
#include <iostream>
class MyClass {
private:
int* data;
size_t size;
public:
// Constructor allocates dynamic memory
MyClass(size_t s) : size(s), data(new int[s]) {}
// Destructor cleans up the resource
~MyClass() { delete[] data; }
};
Scenario:
void shallowCopyProblem() {
MyClass obj1(5); // Allocates memory
MyClass obj2 = obj1; // Default copy constructor: shallow copy
// When obj1 and obj2 are destroyed, both will attempt to delete the same pointer!
}
Result:
- Both
obj1
andobj2
point to the same dynamically allocated memory. - When the destructor is called for
obj1
andobj2
, it will try to delete the same memory twice, leading to undefined behavior.
Complete Code:
#include <iostream>
class MyClass {
private:
int* data;
size_t size;
public:
// Constructor allocates dynamic memory
MyClass(size_t s) : size(s), data(new int[s]) {}
// Destructor cleans up the resource
~MyClass() { delete[] data; }
};
int main()
{
MyClass obj1(7); // Allocates memory
MyClass obj2 = obj1; // Default copy constructor: shallow copy
// When obj1 and obj2 are destroyed, both will attempt to delete the same pointer!
}
2 Resource Leak on Assignment
The compiler-generated copy assignment operator does not free the existing resources of the target object before assigning new resources. This can lead to memory/resource leaks.
void assignmentProblem() {
MyClass obj1(5); // Allocates memory
MyClass obj2(10); // Allocates memory
obj2 = obj1; // Default assignment operator: shallow copy
// Memory allocated for obj2 (10 elements) is leaked.
}
Result:
- The memory originally allocated for
obj2
is not released before assigningobj1
's resources. - This results in a memory leak.
3 Move Operations: Inefficiency and Undefined Behavior
Without move semantics, temporary objects (e.g., function return values) are handled using copy semantics, which can be unnecessarily expensive. Worse, if move operations are auto-generated but rely on shallow transfers, they can cause resource mismanagement.
// 5. Move Constructor
Employee(Employee&& other) noexcept {
cout << "Move Constructor Called!" << endl;
this->name = other.name; // Steal the resource
this->id = other.id;
other.name = nullptr; // Leave the source in a valid state
other.id = 0;
}
// 6. Move Assignment Operator
Employee& operator=(Employee&& other) noexcept {
cout << "Move Assignment Operator Called!" << endl;
if (this == &other) {
return *this; // Handle self-assignment
}
delete[] name; // Clean up existing memory
this->name = other.name; // Steal the resource
this->id = other.id;
other.name = nullptr; // Leave the source in a valid state
other.id = 0;
return *this;
}
Notice the &&
in the parameters of the move operations. Also, the parameters are not marked as const
as opposed to const parameters in copy operations.
int main() {
Employee employee1("Alice", 100); // Create original object
Employee employee2("Bob", 200); // Create another object
// Test Copy Assignment
employee2 = employee1;
// Test Move Constructor
Employee employee3 = std::move(employee1);
}
Using the std::move
library function, we can force move semantics.
Complete Code:
#include <iostream>
#include <cstring>
using namespace std;
class Employee {
public:
// 1. Default Constructor
Employee() : name(nullptr), id(0) {}
// 2. Parameterized Constructor
Employee(const char* name, int id) {
int length = strlen(name);
this->name = new char[length + 1];
strcpy(this->name, name);
this->id = id;
}
// 3. Copy Constructor (Deep Copy)
Employee(const Employee& other) {
cout << "Copy Constructor Called!" << endl;
if (other.name) {
int length = strlen(other.name);
this->name = new char[length + 1];
strcpy(this->name, other.name);
} else {
this->name = nullptr;
}
this->id = other.id;
}
// 4. Copy Assignment Operator (Deep Copy)
Employee& operator=(const Employee& other) {
cout << "Copy Assignment Operator Called!" << endl;
if (this == &other) {
return *this; // Handle self-assignment
}
delete[] name; // Clean up existing memory
if (other.name) {
int length = strlen(other.name);
this->name = new char[length + 1];
strcpy(this->name, other.name);
} else {
this->name = nullptr;
}
this->id = other.id;
return *this;
}
// 5. Move Constructor
Employee(Employee&& other) noexcept {
cout << "Move Constructor Called!" << endl;
this->name = other.name; // Steal the resource
this->id = other.id;
other.name = nullptr; // Leave the source in a valid state
other.id = 0;
}
// 6. Move Assignment Operator
Employee& operator=(Employee&& other) noexcept {
cout << "Move Assignment Operator Called!" << endl;
if (this == &other) {
return *this; // Handle self-assignment
}
delete[] name; // Clean up existing memory
this->name = other.name; // Steal the resource
this->id = other.id;
other.name = nullptr; // Leave the source in a valid state
other.id = 0;
return *this;
}
// 7. Destructor
~Employee() {
delete[] name; // Free allocated memory
cout << "Destructor Called!" << endl;
}
void print() const {
cout << "Name = " << (name ? name : "null") << ", Address = " << static_cast<void*>(name) << endl;
cout << "Id = " << id << endl;
}
private:
char* name; // Dynamically allocated memory for name
int id; // Employee ID
};
int main() {
Employee employee1("Alice", 100); // Create original object
Employee employee2("Bob", 200); // Create another object
cout << "Before Assignment:\n";
cout << "Employee 1: ";
employee1.print();
cout << "Employee 2: ";
employee2.print();
// Test Copy Assignment
employee2 = employee1;
cout << "\nAfter Copy Assignment:\n";
cout << "Employee 1: ";
employee1.print();
cout << "Employee 2: ";
employee2.print();
// Test Move Constructor
Employee employee3 = std::move(employee1);
cout << "\nAfter Move Constructor:\n";
cout << "Employee 1 (Moved From): ";
employee1.print();
cout << "Employee 3: ";
employee3.print();
// Test Move Assignment
employee2 = std::move(employee3);
cout << "\nAfter Move Assignment:\n";
cout << "Employee 3 (Moved From): ";
employee3.print();
cout << "Employee 2: ";
employee2.print();
return 0;
}
Before Assignment:
Employee 1: Name = Alice, Address = 0x505488
Id = 100
Employee 2: Name = Bob, Address = 0x505498
Id = 200
Copy Assignment Operator Called!
After Copy Assignment:
Employee 1: Name = Alice, Address = 0x505488
Id = 100
Employee 2: Name = Alice, Address = 0x505498
Id = 100
Move Constructor Called!
After Move Constructor:
Employee 1 (Moved From): Name = null, Address = 0
Id = 0
Employee 3: Name = Alice, Address = 0x505488
Id = 100
Move Assignment Operator Called!
After Move Assignment:
Employee 3 (Moved From): Name = null, Address = 0
Id = 0
Employee 2: Name = Alice, Address = 0x505488
Id = 100
Destructor Called!
Destructor Called!
Destructor Called!