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:

  1. Destructor: Releases the acquired resource when the object falls out of scope.
  2. Copy Constructor: Creates a new instance by performing a deep copy of an existing instance's resources.
  3. Copy Assignment Operator: Releases the current instance's existing resources and replaces them with a deep copy of another instance's resources.
  4. Move Constructor: Transfers ownership of resources from an rvalue to a new instance, leaving the source object in a valid, destructible, but empty state.
  5. 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 noexcept on Move Operations: Move constructors and move assignment operators should be marked noexcept. Standard library containers like std::vector check for noexcept before 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, or std::string—the compiler-generated default implementations of the five special functions are almost always correct, eliminating the need to write any of them manually.