CS 70

Providing a Copy Constructor

  • Pig speaking

    I want MORE constructors!!! I want to be able to copy Barns!

  • LHS Cow speaking

    Okay, but remember, you only need to write a copy constructor if someone is going to use it. It's a waste of time to write code no one wants to use.

  • Pig speaking

    But MORE is better!!

  • Dog speaking

    Can we just say = default and let the system synthesize it?

  • LHS Cow speaking

    No! If the compiler synthesizes a copy constructor for us, the code will do the wrong thing!

  • Hedgehog speaking

    If I write it, it'll probably be wrong.

  • LHS Cow speaking

    And that's one reason not to write code you don't need to write.

Recall that job of a copy constructor is to initialize a new instance of a class to look exactly like a preexisting instance of the class.

C++ can synthesize a copy constructor for us. It'll do that if we say = default (and it'll also usually do so if we if we do't specify anything).

C++'s synthesized copy constructor will make a copy of each of the data members in the class, one at a time. In some cases, this is great (it works fine in our Cow class, for example), but sometimes it's totally wrong. Let's see why…

As a reminder, here's what a Barn with two Cows looks like in memory.

Look at the memory model for an instance of the Barn class, and then explain why the synthesized copy constructor will cause a problem for the Barn class.

Let's take a look at what would happen with a synthesized copy constructor:

Beware of Shallow Copies!

The kind of copy done by the synthesized copy constructor is called a shallow copy.

At the surface, it looks like our Barn was copied, but if we dig any deeper than the data members themselves, we will find that the two objects actually share memory! This arrangement violates the principle of ownership—only one Barn can own the cows, but now two Barns both think they own the same cows!

Making a Deep Copy

To fix this issue, we need to write our own copy constructor that will create a deep copy.

A deep copy will go through and make copies of all of the heap memory being used by the Barn object, so that we'll have a truly separate Barn with separate cows!

Barn::Barn(const Barn& other) :
    cowCapacity_{other.cowCapacity_},
    cowCount_{other.cowCount_},
    residentCows_{new Cow*[cowCapacity_]}
{
    for (size_t i = 0; i < other.cowCount_; ++i) {
        // Fetch pointer to the Cow we want to copy from their array
        Cow* otherCowPtr = other.residentCows_[i];
        // Make a copy.
        Cow* duplicateCowPtr = new Cow{*otherCowPtr};
        // Store it in our array.
        residentCows_[i] = duplicateCowPtr;
    }
}

You could also write the loop as follows, using the start/past-the-end pointer approach (that we saw in Week 5 Lesson 1):

{
    for (Cow** p = other.residentCows_; p < other.residentCows_ + other.cowCount_; ++p) {
        // Fetch pointer to the Cow we want to copy from their array
        Cow* otherCowPtr = *p;
        // Make a copy.
        Cow* duplicateCowPtr = new Cow{*otherCowPtr};
        // Store it in our array.
        residentCows_[i] = duplicateCowPtr;
    }
}

What is otherCowPointer?

The inside of the for loop is going to look a lot like our addCow function, except that we're not updating the cowCount_. By starting with a cowCount_ of 0, we can leverage the function we already wrote:

Barn::Barn(const Barn& other) :
    cowCapacity_{other.cowCapacity_},
    cowCount_{0},
    residentCows_{new Cow*[cowCapacity_]}
{
    for (size_t i = 0; i < other.cowCount_; ++i) {
        // Fetch pointer to the Cow we want to copy from the array
        Cow* otherCowPtr = other.residentCows_[i];
        // Make a copy.
        addCow(*otherCowPtr);
    }
}

Alternatively, you could write the loop using the start/past-the-end pointer approach (that we saw in Week 5 Lesson 1):

{
    for (Cow** p = other.residentCows_; p < other.residentCows_ + other.cowCount_; ++p) {
        // Fetch pointer to the Cow we want to copy from the array
        Cow* otherCowPtr = *p;
        // Make a copy.
        addCow(*otherCowPtr);
    }
  • Duck speaking

    In that second loop, could I have skipped defining otherCowPtr and just written addCow(**p);?

  • LHS Cow speaking

    Yes, if you wanted.

  • Duck speaking

    And it would be more efficient?

  • LHS Cow speaking

    No. The compiler isn't stupid. When optimization is turned on, just about any way we'd write these loops will probably produce the exact same machine code.

  • RHS Cow speaking

    Write your code so it is as easy to understand as possible.

  • Duck speaking

    I think addCow(**otherCowPtr); is easy to understand.

  • Hedgehog speaking

    I don't.

Tip: Whenever you have code that would put two stars together, you can usually rewrite it to be less confusing by adding a variable explaining what is going on.

On Online GDB is a slightly more elegant version of the code that reuses our existing code even more.

  • Pig speaking

    Can I have MORE video explanation of this stuff?

  • Duck speaking

    And can we do it my short-and-sweet way?

  • LHS Cow speaking

    You're in luck, Duck. Remaking this old video is on our to-do list, but for now, you get your way. Sorry Hedgehog.

  • Hedgehog speaking

    I don't mind. This way I get to see my friend Prof. Medero.

Any Privacy Concerns?

  • Horse speaking

    Whoah, whoah! Wait a second!

  • LHS Cow speaking

    What's up?

  • Horse speaking

    We can't even access other.residentCows_, can we? It's private! Can we access the private contents of a different object?

  • LHS Cow speaking

    Ah… well, private access means that no code outside the class can access that member. Objects that are instances of the same class can access each others' private stuff.

  • RHS Cow speaking

    In this case, other is a Barn and this function is in the Barn class, so it's fine.

(When logged in, completion status appears here.)