Deep Copies and The Big Three

In this lecture, we'll introduce the idea of shallow copies vs. deep copies, its connection to dynamic resource management, and the way these concepts are realized specifically in C++ via the Big Three.

Updated Fall 2024

1: Warm Up Exercise
1.1 Not Started
1.1 Exercise: Warm Up

Consider the code below for the UnsortedIntSet class from previous lectures, which we've recently upgraded to use a pointer to a dynamically allocated array (instead of storing the array directly).

The code also contains a main() function that creates two sets:

  • The first set, s1, is default-constructed and we add some elements to it.
  • Then, a second set s2 is created as a copy of s1.

Go ahead and run the lobster simulation (you can just click "run" to skip all the way to the end). Then, observe the contents of memory and the structure of the two sets. Can you identify any potential problems that might lead to unintuitive behavior?


You're welcome to check your solution with this walkthrough video:




2: The Shallow Copy Problem
2.1 Not Started

Let's take a look at the built-in copying behavior we get in C++ for compound objects (i.e. struct or class) and the way this leads to a "shallow copy" by default.


2.1 Exercise: Shallow Copy Problems

Consider the code for UnsortedSet below. The implicitly-defined copy constructor is used for the line UnsortedSet<int> s2 = s1; in main(), but this only performs a shallow copy, which results in some problems.

template <typename T>
class UnsortedSet {
public:

  // Default constructor, empty set
  UnsortedSet()
    : elts(new T[DEFAULT_CAPACITY]), capacity(DEFAULT_CAPACITY), elts_size(0) { }

  // Built-in copy ctor from compiler. (Normally, this wouldn't be written
  // out. But we've done so here to emphasize what the built-in one does
  // behind the scenes.)
  UnsortedSet(const UnsortedSet &other)
    : elts(other.elts), capacity(other.capacity), elts_size(other.elts_size) { }

  // Destructor
  ~UnsortedSet() { delete[] elts; }

  // grow function switches to a new, larger array
  void grow() {
    T *newArr = new T[2 * capacity];
    for (int i = 0; i < elts_size; ++i) {
      newArr[i] = elts[i];
    }
    capacity *= 2;
    delete[] elts;
    elts = newArr;
  }
};
Part 1: In the code below, what problem is encountered on the marked line that causes undefined behavior? You might find it helpful to sketch out a memory diagram and trace the code.
Part 2: In the code below, the shallow copy is still made, but we don't modify either set after making the problematic copy. Is this ok? Or is there still some memory error that will occur?
int main() {
  // assume initial capacity of 2
  UnsortedSet<int> s1;
  s1.insert(2);
  s1.insert(3);

  UnsortedSet<int> s2 = s1;

  // this requires a call to grow()
  s2.insert(4);
  cout << s1 << endl; // THIS LINE
}
int main() {
  // assume initial capacity of 2
  UnsortedSet<int> s1;
  s1.insert(2);
  s1.insert(3);

  UnsortedSet<int> s2 = s1;

  // read only operations - is this ok?
  cout << s1 << endl;
  cout << s2 << endl;
}


You're welcome to check your solution with this walkthrough video:




3: Deep Copy Constructors
3.1 Not Started

The semantically correct way to copy an UnsortedSet object (or any class that manages a dynamic resource, like the underlying array for the set) is to implement a deep copy. We can do this by defining our own custom copy constructor for the class.


3.1 Exercise: UnsortedIntSet Copy Constructor

Implement a custom copy constructor for UnsortedIntSet. Your implementation should ensure that a deep copy of the dynamically allocated array is made, following these steps:

  1. Initialize the "regular" members (capacity and elts_size) of the new set to match the original set.
  2. Initialize elts to point to a new dynamically allocated array, with the same capacity as the array from the original set.
  3. Copy over each element from the original set into the new set's dynamic array.

(Hint: Steps 1 and 2 should be done in the member-initializer-list, and 3 uses a loop in the body of the constructor.)

The main() function provided includes testing code to verify your implementation.


You're welcome to check your solution with this walkthrough video:




4: Deep Copy Assignment
4.1 Not Started

Copies are also made when we perform assignment on already-existing objects (as opposed to declaring a completely new object as a copy of another). The key difference is that in this case, the assigned-to object will already have some prior dynamic resources that need to be cleaned up before the deep copy is made.

Additionally, to implement the deep copy properly in C++, we can conveniently overload the = operator.


4.1 Exercise: UnsortedIntSet Assignment Operator

Implement a custom copy constructor for UnsortedIntSet. Your implementation should ensure that a deep copy of the dynamically allocated array is made, following these steps:

  1. If the assignment is a self-assignment, simply return *this;.
  2. Free the original dynamically allocated array using delete.
  3. Copy over the "regular" members (capacity and elts_size) from the rhs set.
  4. Make a deep copy by setting elts to point to a new dynamically allocated array, with the same capacity as the array from the rhs set.
  5. Copy over each element from the rhs set into the new dynamic array.
  6. return *this;

The main() function provided includes testing code to verify your implementation.


You're welcome to check your solution with this walkthrough video:




5: The Big Three
5.1 Not Started

Finally, let's take a look at the connection between dynamic resource management with destructors and the necessity for a deep copy via a custom copy constructor and assignment operator. Affectionately, these are called "the big three" - and it turns out that they come as a package deal. Here's some more details:


5.1 Exercise: The Big Three

Determine what is printed by the following code. To do this, you'll need to think about where each of the Big Three are used by the code in the main program. Record your prediction in the box at the right. You can use the simulation to double check your answer.

Note: The run button in the simulation automatically pauses at the end of main. If you want to see all the output, including the destructors that run as main ends, you can click "run", "step", then "run" again.

Record your predicted output here.


You're welcome to check your solution with this walkthrough video:


You've reached the end of this lecture! Your work on any exercises will be saved if you re-open this page in the same web browser.

Participation Credit
Make sure to sign in to the page, complete each of the exercises, and double check the participation indicator at the top left of this page to ensure you've earned credit.