Skip to main content

Section 22.14 Rule of Three

Together, the destructor, copy constructor, and assignment operator are known as the Rule of Three. If you create a class that manages resources, it should declare all three of these functions so that it can properly manage the resources it owns. If you do not think you need a copy constructor and/or assignment operator (because you do not plan on letting your class by copied), you should declare them as = delete; to prevent the compiler from generating default versions that will do shallow copies. This way, if someone accidentally tries to copy an object of your class, the compiler will produce an error instead of generating flawed code. Here is a sample of declaring that there is no copy constructor or assignment operator:
Listing 22.14.1.
class MyClass {
public:
    MyClass();  // we will implement a constructor
    ~MyClass(); // we will implement a destructor

    // we do not want to allow copying
    MyClass(const MyClass& other) = delete; // no copy constructor
    MyClass& operator=(const MyClass& other) = delete; // no assignment operator
    ...

Note 22.14.1.

There is also something known as the Rule of Five. This extends the Rule of Three to include a β€œmove constructor” and a β€œmove assignment operator”. These operate by stealing resources from the original and moving them to the new object, rather than copying them. These are not required for correct behavior, so we are not covering them in this book. They can however improve performance in situations where we are copying an object that is managing resources and know that the original is no longer needed once the copy is complete.
Also worth clarifying is the difference between the copy constructor and the assignment operator. Using the = symbol can either call the copy constructor or the assignment operator. The assignment operator is used when we are copying the contents from one existing object to another existing object, while the copy constructor is used to create a new object as a copy of an existing object.
Listing 22.14.2.
PlayerList p1(3);  // list with space for 3 players
...

// Explicitly call the copy constructor to copy p2 into p1
PlayerList p2(p1);

// p3 does not exist yet, so this implicitly calls the copy constructor
// even though it looks like an assignment
PlayerList p3 = p1;

PlayerList p4(10);  // list with space for 10 players
// p4 already exists, so this calls the assignment operator
p4 = p1;
Remembering the distinction between = as an assignment operator and = as a copy constructor is not critical - you generally need to implement both the copy constructor and the assignment operator anyway. But it can help explain why the compile or AddressSanitizer may generate an error related to your copy constructor when you write something like PlayerList p3 = p1;.
With that, here is the final version of our class to manage a dynamic array of strings. The Rule of Three code is highlighted.
Listing 22.14.3. PlayerList final version
module;

#include <iostream>
#include <string>

export module PlayerList;

using namespace std;

export class PlayerList {
private:
    string* m_players; // pointer to the array of player names
    int m_size; // number of players in the list
public:
  PlayerList(int size);

  PlayerList(const PlayerList& other); // Copy constructor

  PlayerList& operator=(const PlayerList& other); // Assignment operator

  ~PlayerList();  // Destructor

  void setPlayerName(int index, const string& name);

  void print() const;
};

PlayerList::PlayerList(int size) {
    if (size <= 0) {
        throw invalid_argument("Size must be positive");
    }
    m_size = size;
    m_players = new string[m_size]; // allocate memory for the array
}

PlayerList::PlayerList(const PlayerList& other) {
    // Copy non-pointer members
    m_size = other.m_size;

    // Allocate own dynamic storage
    m_players = new string[m_size];

    // Copy the contents from the other PlayerList
    for (int i = 0; i < m_size; ++i) {
        m_players[i] = other.m_players[i];
    }
}

PlayerList& PlayerList::operator=(const PlayerList& other) {
    if (this != &other) { // Check for self-assignment
        // Deallocate existing memory
        delete[] m_players;

        // Copy non-pointer members
        m_size = other.m_size;

        // Allocate own dynamic storage
        m_players = new string[m_size];

        // Copy the contents from the other PlayerList
        for (int i = 0; i < m_size; ++i) {
            m_players[i] = other.m_players[i];
        }
    }
    return *this;
}

PlayerList::~PlayerList() {
    delete[] m_players; // deallocate memory for the array
}

void PlayerList::setPlayerName(int index, const string& name) {
    if (index < 0 || index >= m_size) {
        throw out_of_range("Index out of range");
    }
    m_players[index] = name; // set the player name at the given index
}

void PlayerList::print() const {
    cout << "Player List: ";
    for (int i = 0; i < m_size; ++i) {
        cout << m_players[i] << " ";
    }
    cout << endl;
}

Checkpoint 22.14.1.

You have attempted of activities on this page.