Copying Data

Steven J. Zeil

Last modified: Jul 16, 2014

Contents:
1. Destructors
2. Copy Constructors
2.1 Where Do We Use a Copy Constructor?
2.2 Compiler-Generated Copy Constructors
2.3 Example: Bid: Compiler-Generated Copy Constructor
2.4 Example: BidCollection: Compiler-Generated Copy Constructor
2.5 Writing a BidCollection Copy Constructor
2.6 Shallow & Deep Copying
3. Assignment
3.1 Compiler-Generated Assignment Ops
3.2 Implementing Assignment
4. The Rule of the Big 3

Copying Data Structures

Copying data is one of the most common operations in C++.


But, First…

A slight diversion …

1. Destructors


Destructors

Destructors are used to clean up objects that are no longer in use.


Destructors are Never Called Explicitly

The compiler generates all calls to destructors implicitly


Implicit Destructor Call 1

If we write

void foo (int d, int c)
{
  Money m (d,c);
  for (int i = 0; i < 100; ++i)
    {
       Money mon2 = m;
        ⋮
    }
}

the compiler generates

void foo (int d, int c)
{
  Money m (d,c);
  for (int i = 0; i < 100; ++i)
    {
       Money mon2 = m;
        ⋮
       mon2.~Money();
    }
  m.~Money();
}


Implicit Destructor Call 2

If we write

void foo (int d, int c)
{
  Money* m  = new Money (d,c);
        ⋮
  delete m;
}

the compiler generates

void foo (int d, int c)
{
  Money* m  = new Money (d,c);
        ⋮
  m->~Money();
  free(m);
}


A Typical Destructor

We use destructors to clean up data “owned” by our structured types:

class BidCollection {

  int MaxSize;
  int size;
  Bid* elements; // array of bids 

public:
  BidCollection (int MaxBids = 1000);

  ~BidCollection ();
    ⋮


Implementing the Destructor

The destructor would be implemented as

BidCollection::~BidCollection()
{
  delete [] elements;
}


Compiler-Provided Destructors

If you don’t provide a destructor for a class, the compiler generates one for you automatically.


A Compiler-Generated Destructor

time.h

auctionBig3/bids.h


Trusting the Compiler-generated Destructor

What would happen if we trusted the compiler-generated destructor for BidCollection?

Answer


Compiler-generated destructors are wrong when…

The compiler-generated destructor will leak memory when

Under those circumstances, the compiler-generated destructor will cause you program to leak memory.

The fix is to implement your own destructor for such classes, and explicitly clean up your mess on the heap:

void NidCollection::~BidCollection()
{
   delete [] elements;
}

2. Copy Constructors

The copy constructor for a class Foo is the constructor of the form:

Foo (const Foo& oldCopy);

2.1 Where Do We Use a Copy Constructor?

The copy constructor gets used in 5 situations:

2.2 Compiler-Generated Copy Constructors

If we do not create a copy constructor for a class, the compiler generates one for us.

2.3 Example: Bid: Compiler-Generated Copy Constructor

struct Bid {
  std::string bidderName;
  Money amount;
  std::string itemName;
  Time bidPlacedAt;

};

Bid does not provide a copy constructor, so the compiler generates one for us, just as if we had written:

struct Bid {
  std::string bidderName;
  Money amount;
  std::string itemName;
  Time bidPlacedAt;

  Bid (const Bid&);
};
  ⋮
Bid::Bid (const Bid& b)
  : bidderName(b.bidderName), amount(b.amount),
    itemName(b.itemName), bidPlacedAt(b.bidPlacedAt)
{}

and that’s probably just fine.

2.4 Example: BidCollection: Compiler-Generated Copy Constructor

struct BidCollection {
  int MaxSize;
  int size;
  Bid* elements; // array of bids 

  /**
   * Create a collection capable of holding the indicated number of bids
   */
  BidCollection (int MaxBids = 1000);

  ~BidCollection ();
  
  /**
   * Read all bids from the indicated file
   */
  void readBids (std::string fileName);
};

BidCollection does not provide a copy constructor, so the compiler generates one for us, just as if we had written:

struct BidCollection {
  int MaxSize;
  int size;
  Bid* elements; // array of bids 

  /**
   * Create a collection capable of holding the indicated number of bids
   */
  BidCollection (int MaxBids = 1000);
  BidCollection (const BidCollection&);
};
  ⋮
BidCollection::BidCollection (const BidCollection& bc)
  : MaxSize(bc.MaxSize), size(bc.size),
    elements(bc.elements)
{}

which is not good at all!


Example: BidCollection is hard to copy

To see why, suppose we had some application code:

bidCollApplication.cpp

Assume we start with this in bids.

When removeEarly is called, we get a copy of bids .

BidCollection removeEarly (BidCollection bc, Time t)
{
  for (int i = 0; i < x.size;)
    {
     if (bc.elements[i].bidPlacedAt < t)
        removeElement (elements, size, i);
     else
        ++i;
    }
  return bc;
}
    ⋮
BidCollection afterNoonBids =
   removeEarly (bids, Time(12,0,0));

removeEarly removes the first morning bid from bc.

Then removeEarly removes the remaining morning bid from bc.

The return statement makes a copy of bc, which is stored in afterNoonBids


Trouble: bids is corrupted

Note that we have corrupted the original collection, bids


That’s not the worst of it!

When we exit removeEarly,

BidCollection removeEarly (BidCollection bc, Time t)
{
  for (int i = 0; i < x.size;)
    {
     if (bc.elements[i].bidPlacedAt < t)
        removeElement (elements, size, i);
     else
        ++i;
    }
  return bc;
}

the destructor for BidCollection is called on bc

BidCollection::~BidCollection()
{
  delete [] elements;
}

Taking us from this …


What a Mess!

… to this.

Both collections have “dangling pointers”


Avoiding this Problem

We could

2.5 Writing a BidCollection Copy Constructor

BidCollection::BidCollection (const BidCollection& bc)
  : MaxSize (bc.MaxSize), size (bc.size)
{
  elements = new Bid[MaxSize];
  for (int i = 0; i < size; ++i)
    elements[i] = bc.elements[i];
}

Once More, With Feeling!

With this copy constructor in place, things should go much more smoothly.

bidCollApplication.cpp

When removeEarly is called, we get a copy of bids.

removeEarly removes the first morning bid from bc.

Then removeEarly removes the remaining morning bid from bc.

The return statement makes a copy of bc, which is stored in afterNoonBids.

But our new copy constructor actually creates a new array for afterNoonBids, copying the bids from bc’s array.


Much Nicer

The destructor for BidCollection is called on bc

2.6 Shallow & Deep Copying

If We Never Write Our Own

If our data members do not have explicit copy constructors (and their data members do not have explicit copy constructors, and … )


Shallow vs Deep Copy

Copy operations are distinguished by how they treat pointers:


Shallow copy is wrong when…


Compiler-generated copy constructors are wrong when…

3. Assignment

Copy constructors are not the only way we make copies of data.

3.1 Compiler-Generated Assignment Ops

If you don’t provide your own assignment operator for a class, the compiler generates one automatically.


Example: BidCollection: Guess what happens

Our BidCollection class has no assignment operator, so the code below uses the compiler-generated version.

Suppose we had some application code:

bidCollAsst.cpp

Assume we start with this in bids.

After the assignment, we have copied bids, bit-by-bit, into bc.

bidCollAsst2.cpp

We remove the first morning bid from bc.

bidCollAsst3.cpp

Then remove the remaining morning bid from bc.


bids is corrupted

Note that we have corrupted the original collection, bids

This should look very familiar!


Avoiding this Problem

We have already seen that, for this class, we need to implement our own copy constructor:

BidCollection::BidCollection (const BidCollection& bc)
  : MaxSize (bc.MaxSize), size (bc.size)
{
  elements = new Bid[MaxSize];
  for (int i = 0; i < size; ++i)
    elements[i] = bc.elements[i];
}

For all the same reasons, we will want to implement our own assignment operator.

3.2 Implementing Assignment

BidCollection& BidCollection::operator= 
     (const BidCollection& bc)
{
  MaxSize = bc.MaxSize;
  size = bc.size;
  delete [] elements;

  elements = new Bid[MaxSize];
  for (int i = 0; i < size; ++i)
    elements[i] = bc.elements[i];

  return *this;
}

*Why return this?**


Self-Assignment

When we do x = x we expect that nothing really changes.

Question: What would happen if we did

bids = bids;

using this assignment operator:

BidCollection& BidCollection::operator= 
     (const BidCollection& bc)
{
  MaxSize = bc.MaxSize;
  size = bc.size;
  delete [] elements;

  elements = new Bid[MaxSize];
  for (int i = 0; i < size; ++i)
    elements[i] = bc.elements[i];

  return *this;
}

?

Answer


Self-Assignment – Really?

May seem an unlikely occurrence, but can often happen in a disguised form, e.g., during array processing,

bidSet[i] = bidSet[j];

Coping with Self-Assignment

To allow for this possibility, most assignment operator functions check to see if the variable on the left (this) is at the same address as the variable on the right:

BidCollection& BidCollection::operator= (const BidCollection& bc)
{
  if (this != &bc)
    {
     MaxSize = bc.MaxSize;
     size = bc.size;
     delete [] elements;

     elements = new Bid[MaxSize];
     for (int i = 0; i < size; ++i)
       elements[i] = bc.elements[i];
    }
  return *this;
}

4. The Rule of the Big 3

A Question of Trust


A Question of Trust

Notice a pattern?


The Rule of the Big 3

If you provide your own version of any one of the Big 3, you should provide your own version of all three.


Providing the Big 3

bidcollection.h

bidcollection.cpp

biddercollection.h

biddercollection.cpp

itemcollection.h

itemcollection.cpp