What is the Rule of Three in C++? | Explained

The Rule of Three is a programming concept generally followed in C++ to ensure safe code and proper management of (memory) resources. It is not always compulsory to follow it, but doing so is considered good practice, and it produces better code.


Rule of Three Explained

The Rule of Three states, that if a Class explicitly defines any of the following special member functions:

  1. Destructor
  2. Copy Constructor
  3. Copy Assignment Operator

Then it should also define the other two as well. So if a Class explicitly defines the Destructor, then it should define the Copy Constructor and the Copy Assignment Operator as well.

Recap

Let’s do a little re-cap of these three member functions, and briefly go over when, and why there are used.

  1. Destructor: Used to execute before the Class object destroys itself. Deallocates memory that has been assignment to the object. Most common use case is when we are using dynamic memory in a Class, so we need to deallocate it with the Destructor.
  2. Copy Constructor: Used to initialize an object using another object of the same class. It “copies” over all of the contents of that object, into the new object.
  3. Copy Assignment Operator: The same as the Copy Constructor, but instead of creating new object using a pre-existing object, it is used between two pre-existing objects to copy over values from one to the other.

It is important to remember that there are compiler generated versions of these member functions, known as the default member functions, i.e. Default Destructor and so on. When we explicitly define any of these, the Default implementation is removed for that member function.

The Rule of three states that if any one of these has to be defined by the programmer out of necessity, then it means that the compiler-generated version of the member function does not fit the needs of the Class. And it is assumed, that since it the default version fails in one case, there is a good chance that the other default versions will not be suitable for that Class either.

Thus, the Rule of Three arrives at the conclusion that all three must be defined in such a case.


Rule of Three – Example

Let’s take a look at an actual example where the Rule of Three is compulsory.

When dealing with dynamic memory in Classes, we often need to define a Destructor (to free up memory) as shown in the Class example below.

class MyClass {
public:
    MyClass(int n) {
        _mem = new int[n];
    }

    ~MyClass() {
        delete[] _mem;
    }

private:
    int* _mem;
};

But this is not complete. According to the Rule of Three, we need to define the Copy Constructor and Copy Assignment Operator. This is because there will be various issues caused when using the Default Implementations.

There is a common issue that occurs when dealing with Dynamic Memory, that when using the default Copy Constructor and Copy Assignment Operator, the contents of the dynamic memory are not copied over, rather only the pointer to that memory is. See this article on Shallow/Deep Copy to learn more about this problem.

So we end up with the following code, which is in accordance to the Rule of Three.

#include <iostream>
using namespace std;

class MyClass {
public:
    MyClass(int n) {       // Constructor
        _mem = new int[n];
        _size = n;
    }

    ~MyClass() {      // Destructor
        delete[] _mem;
    }

    MyClass (const MyClass &other) {     // Copy Constructor
        _size = other._size;
        _mem = new int[_size];

        for (int i = 0; i < _size; i++) 
            _mem[i] = other._mem[i];
    }

    MyClass& operator=(const MyClass& other) {    // Copy Assignment Operator
        if (this == &other) return *this;

        delete[] _mem;
        _mem = NULL;
        _size = other._size;
        _mem = new int[_size];

        for (int i = 0; i < _size; i++) 
            _mem[i] = other._mem[i];

        return *this;
    }

public:
    int* _mem;
    int _size;
};

This code is now safe, and will work as expected for almost any scenario.


Rule of Five in C++

With the introduction of C++11, and the new move semantic features, the Rule of Three expanded into the Rule of Five. There are now five special member functions that should be defined, in the event that any one of these have been defined explicitly.

  1. Destructor
  2. Copy Constructor
  3. Copy Assignment Operator
  4. Move Constructor
  5. Move Assignment Operator

The first three are the same as the Rule of Three, but now we have two new additions. The Move Constructor and Move Assignment Operator are a more optimized way of creating and copying objects.

The basic idea behind them, is that instead of creating extra copies of the same memory each time, and allocating and deallocating them separately, we deal with just one instance of the memory that gets “moved” around instead of copied. In a nutshell, Move Semantics helps reduce the number of Constructor/Operator calls that occur, improving performance.

Let’s take a look at a slightly different example, regarding the creation of a String Class. This example follows the Rule of Five, and has all Five Special Member Functions available in it. ( We have discussed the Rule of Five, and the below example in greater detail in it’s own dedicated tutorial here)

#include <iostream>
#include <cstring>
using namespace std;

class String {
    char * _data;
    int _size;

public:
    String(const char* data) {   // Constructor
        cout << "Created" << endl;
        _size = strlen(data);
        _data = new char[_size];
        memcpy(_data, data, _size);
    }

    String(String&& str) {   // Move Constructor
        cout << "Moved" << endl;
        _size = str._size;
        _data = str._data;

        str._data = nullptr;
    }

    String& operator=(String&& str) {  // Move Assignment Constructor
        if (this != &str) {
            delete[] _data;       // Free Original Memory
            _data = str._data;   // Copy the other string's Pointer into this string.
            str._data = nullptr;  // Reset the other string's data pointer.
        }
        return *this;
    }

    String& operator=(const String& str) {
        if (this == &str) return *this;

        delete[] _data;
        _data = NULL;
        _size = str._size;
        _data = new char[_size];
        memcpy(_data, str._data, _size);

        return *this;
    }

    String(const String& str) {   // Copy Constructor
        cout << "Copied" << endl;
        _size = str._size;
        _data = new char[_size];
        memcpy(_data, str._data, _size);
    }

    ~String() {    // Destructor
        cout << "Destroyed" << endl;
        delete[] _data;
    }

    void Print() const {
        for (size_t i = 0; i < _size; i++)
            cout << _data[i];
        cout << endl;
    }
};

class Person {
    String _name;

public:
    Person(const String& s) : _name(s) { }

    Person(String&& s) : _name(std::move(s)) { }

    void PrintName() {
        _name.Print();
    }
};

int main() {
    Person person(String("Coder"));
    person.PrintName();

    cin.get();
}

There is another lesser known “rule” called the Rule of Zero, which emphasizes on not defining any special member functions. Want to know how that works? Follow the tutorial link to learn more.


This marks the end of the “Rule of Three in C++” Tutorial. Any suggestions or contributions for CodersLegacy are more than welcome. Questions regarding the tutorial content can be asked in the comments section.

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments