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:
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
...
Note22.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.
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;.
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;
}