Section 22.13 Assignment Operator
The final responsibility of a container class is to implement the assignment operator. The assignment operator is used to copy the contents of one object to another existing object of the same class:
PlayerList original(3); // Create a PlayerList with space for 3 players
original.setPlayerName(0, "Alice");
original.setPlayerName(1, "Bob");
original.setPlayerName(2, "Carlos");
PlayerList other(1); // Create another PlayerList with space for 1 player
other.setPlayerName(0, "Diana");
other = original; // Use the assignment operator to copy original into other
Right before the assignment, the memory layout looks like this:
Like the copy constructor, there is a default assignment operator that performs a shallow copy. It would copy the pointer to the array from
original
to other
, rather than creating a new array and copying the contents. This would result in leaking (losing track) of the old array that other owned as well as the same problems we saw with the shallow copy constructor:
Instead, we need to delete the old array and then make a deep copy just like the copy constructor did.
There are two other technical wrinkles to consider:
-
We need to handle the case where the object is assigned to itself, like
original = original;
. That would be silly to write, but it is syntactically legal and could happen by accident in more complex code. In this case we do not want to do anything. We certainly do not want to delete the existing array! -
We need to return a reference to the current object so that we can chain assignments like
a = b = c;
. If the assignment operator wasvoid
that syntax would not be possible.
Here is what the assignment operator would look like:
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& operator=(const PlayerList& other); // Assignment operator
...
};
PlayerList& PlayerList::operator=(const PlayerList& other) {
// Check for self-assignment
if (this != &other) {
// 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; // Return the current object
}
...
Keys to notice:
-
The return type is always a reference to the current object type.
-
The parameter is a reference to another object of the same type. It gets the right hand side of the assignment.
-
We first check if the current object’s memory address (
this
) is the same as the other object’s memory address (&other
). If so, we do not need to copy or delete anything. -
If the objects are different, we delete the old array. Then we use the same logic as the copy constructor to make a deep copy.
-
Finally, we return
*this
to allow for chaining assignments.
It looks like a lot. But it really is just a few extra steps on top of the copy constructor and those steps are always the same.
This program demonstrates the issue. After copying
pList
, it changes the name of the second player in the copy. But when we then print out pList
, it has been modified as well. AddressSanitizer alerts us to a heap-use-after-free
in ~PlayerList()
. That is the second destructor call trying to delete the array that has already been deleted.
Things would be even worse if one of the two PlayerLists was scoped to last longer than the other. Whichever one was destroyed first would delete the array and the other would be left with a dangling pointer.
Checkpoint 22.13.1.
We want to add an assignment operator to the NumList class. It manages a dynamic array of integers. Examine the members and decide what needs to be deleted. Then build the function as it would appear when defined outside the class.
classDiagram class NumList { -m_numbers : int* -m_size : int -m_description : string }
Checkpoint 22.13.2.
You have attempted of activities on this page.