Implementing the Rule of Five in Modern C++
The Rule of Five is a core resource management design pattern in modern C++ that states if a class requires an explicit definition of a destructor, copy constructor, or copy assignment operator, it almost certainly requires all three, along with the move constructor and move assignment operator. This article outlines why this idiom is essential for managing dynamically allocated resources safely, examines the role of each of the five special member functions, and provides a clear, practical implementation adhering to standard modern C++ best practices.
Why the Rule of Five Exists
In C++03, developers relied on the "Rule of Three" (destructor, copy constructor, copy assignment operator) to prevent resource leaks and dangling pointers when managing raw handles, such as raw memory or file descriptors. C++11 introduced move semantics to eliminate unnecessary deep-copy operations by allowing resources to be transferred ("moved") from temporary rvalue objects to new or existing lvalue objects.
Because defining any copy operation or destructor suppresses the automatic generation of move operations by the compiler, all five functions must be explicitly declared to support both deep copying and efficient moving.
The Five Special Member Functions
To properly implement the Rule of Five, a class must define:
- Destructor: Releases the acquired resource when the object falls out of scope.
- Copy Constructor: Creates a new instance by performing a deep copy of an existing instance's resources.
- Copy Assignment Operator: Releases the current instance's existing resources and replaces them with a deep copy of another instance's resources.
- Move Constructor: Transfers ownership of resources from an rvalue to a new instance, leaving the source object in a valid, destructible, but empty state.
- Move Assignment Operator: Releases the current instance's resources and transfers ownership from an rvalue object into the current instance.
Implementation Example
The following example demonstrates a custom dynamic array class
(ResourceManager) implementing all five member
functions:
#include <utility>
#include <cstddef>
#include <algorithm>
class ResourceManager {
private:
int* data_;
std::size_t size_;
public:
// Standard constructor
explicit ResourceManager(std::size_t size)
: data_(size ? new int[size]() : nullptr), size_(size) {}
// 1. Destructor
~ResourceManager() noexcept {
delete[] data_;
}
// 2. Copy Constructor
ResourceManager(const ResourceManager& other)
: data_(other.size_ ? new int[other.size_] : nullptr), size_(other.size_) {
std::copy(other.data_, other.data_ + size_, data_);
}
// 3. Copy Assignment Operator
ResourceManager& operator=(const ResourceManager& other) {
if (this != &other) {
int* new_data = other.size_ ? new int[other.size_] : nullptr;
std::copy(other.data_, other.data_ + other.size_, new_data);
delete[] data_;
data_ = new_data;
size_ = other.size_;
}
return *this;
}
// 4. Move Constructor
ResourceManager(ResourceManager&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
}
// 5. Move Assignment Operator
ResourceManager& operator=(ResourceManager&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
}
return *this;
}
// Member accessors
std::size_t size() const noexcept { return size_; }
int* data() noexcept { return data_; }
};Key Considerations for Modern C++
- Use
noexcepton Move Operations: Move constructors and move assignment operators should be markednoexcept. Standard library containers likestd::vectorcheck fornoexceptbefore moving elements during reallocation; if the move operation can potentially throw, the container falls back to expensive copy operations to maintain strong exception guarantees. - Leave Moved-From Objects in a Valid State: After a
move operation, the source object must remain in a state that safely
permits destruction and reassignment, typically achieved by setting its
internal resource pointers to
nullptr. - The Rule of Zero: While mastering the Rule of Five
is critical for writing custom low-level resource wrappers, idiomatic
modern C++ advises applying the "Rule of Zero." By composing classes
using types that already manage their own resources—such as
std::unique_ptr,std::shared_ptr,std::vector, orstd::string—the compiler-generated default implementations of the five special functions are almost always correct, eliminating the need to write any of them manually.