The Rule of Three is a C++ programming guideline that helps classes manage dynamically allocated resources safely. It states that if a class defines one of the special member functions related to resource management, it should usually define all three.
- Applies to classes that own resources such as dynamic memory.
- Ensures resources are copied and released correctly.
Need for the Rule of Three
The compiler-generated copy constructor and copy assignment operator perform member-wise (shallow) copying, which is sufficient for simple classes. However, when a class owns dynamically allocated memory or other resources, shallow copying causes multiple objects to share the same resource.
- Multiple objects may point to the same dynamically allocated memory.
- Destroying one object may invalidate the resource used by another.
- Can result in dangling pointers, double deletion, and undefined behavior.
- Custom implementations are required to safely manage owned resources.
Components of the Rule of Three
The Rule of Three consists of three special member functions that work together to correctly manage resources owned by a class.
Destructor
The destructor is responsible for releasing resources owned by an object before it is destroyed.
- Frees dynamically allocated memory.
- Closes files, sockets, or other acquired resources.
- Prevents resource leaks.
Copy Constructor
The copy constructor creates a new object by copying an existing object.
- Invoked when a new object is initialized from another object.
- Creates an independent copy of owned resources.
- Prevents multiple objects from sharing the same memory.
Copy Assignment Operator
The copy assignment operator copies one existing object into another existing object.
- Invoked after both objects have already been created.
- Replaces the resources of the destination object safely.
- Usually handles self-assignment and releases old resources before copying.
Example Without the Rule of Three
The following example defines a destructor but relies on the compiler-generated copy constructor and copy assignment operator.
#include <algorithm>
#include <iostream>
using namespace std;
class Array {
int size;
int* vals;
public:
Array(int s, int* v)
{
size = s;
vals = new int[size];
copy(v, v + size, vals);
}
// Destructor only
~Array()
{
delete[] vals;
}
};
int main()
{
int arr[] = {10, 20, 30};
Array a1(3, arr);
// Compiler-generated copy constructor
Array a2 = a1;
return 0;
}
Output
Undefined behaviorExplanation: The compiler-generated copy constructor performs a shallow copy.
- Both objects share the same memory.
- Destroying both objects attempts to free the same memory.
- This results in undefined behavior.
Example Following the Rule of Three
A class that manages dynamic memory should define all three special member functions.
#include <algorithm>
#include <iostream>
using namespace std;
class Array {
int size;
int* vals;
public:
// Constructor
Array(int s, int* v)
{
size = s;
vals = new int[size];
copy(v, v + size, vals);
}
// Copy Constructor
Array(const Array& other)
{
size = other.size;
vals = new int[size];
copy(other.vals, other.vals + size, vals);
}
// Copy Assignment Operator
Array& operator=(const Array& other)
{
if (this != &other)
{
delete[] vals;
size = other.size;
vals = new int[size];
copy(other.vals, other.vals + size, vals);
}
return *this;
}
// Destructor
~Array()
{
delete[] vals;
}
};
int main()
{
int arr[] = {10, 20, 30};
Array a1(3, arr);
// Copy constructor
Array a2 = a1;
// Copy assignment operator
Array a3(3, arr);
a3 = a1;
cout << "Program executed successfully";
return 0;
}
Output
Program executed successfully
Explanation: The class performs a deep copy, so each object manages its own memory.
- Each object has a separate copy of the data.
- The destructor deletes only its own memory.
- This prevents double deletion and resource conflicts.
When to Follow the Rule of Three
The Rule of Three is primarily intended for classes that directly manage resources.
- Classes using dynamic memory allocated with new.
- Classes managing file handles or sockets.
- Classes owning operating system resources.
- Classes responsible for exclusive ownership of any resource.
Note: Classes that only contain primitive data types or standard library containers like std::vector or std::string usually do not require custom implementations because these classes already manage their resources safely.
Advantages of the Rule of Three
Following the Rule of Three improves resource management and program reliability.
- Prevents shallow copy-related errors.
- Avoids memory leaks and double deletion.
- Ensures each object owns its own resources.
- Makes object copying predictable and safe.
- Produces more robust and maintainable code.
Limitations of the Rule of Three
Although useful, implementing the Rule of Three increases the amount of code that must be maintained.
- Requires writing multiple special member functions.
- Incorrect implementations can introduce subtle bugs.
- Does not take advantage of move semantics introduced in C++11.
- Modern C++ often prefers the Rule of Five for resource-managing classes.