2.8. Describe the big 3

The big 3 in C++ class design are

  1. the copy constructor,

  2. the assignment operator, and

  3. the destructor.

For each of these, if you do not provide them, the compiler generates a version for you.

The rule of the big 3 states that,

if you provide your own version of any one of the big 3, you should provide your own version of all 3.

So your choices as a class designer come down to:

  1. The compiler-generated version is acceptable for all three.

  2. You have provided your own version of all three.

  3. You have provided private versions of the copy constructor and assignment operator so no one can use them.

Copy Constructors

Like the default constructor, copy constructors are ordinary constructors that just happens to get used in some special places.

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

Foo (const Foo& oldCopy);

The copy constructor gets used in 5 situations:

  1. When you declare a new object as a copy of an old one:

    Day day2 (day1);

    or

    Day day2 = day1;

  2. When a function call passes a parameter by copy (i.e., the formal parameter does not have a &):

    void foo (Day b, int k);
      <[:]>
    
    Day indepDay (1776, 7, 4);
    foo (indepDay, 0);   // foo actually gets a copy of indepDay
    

  3. When a function returns an object:

    Day foo (int k);
    {
      Day d;
      <[:]>
      return d;
    }
    

  4. When data members are initialized in a constructor's initialization list:

    Contact::Contact (std::string theName, 
                    Address theAddress, long id)
      : <[+]>name(theName)<[-]>, 
    <[+]>    address(theAddress),<[-]>
        identifier(id)
    {
    }
    

  5. When an object is a data member of another class for which the compiler has generated its own copy constructor.

Compiler-Generated Copy Constructors

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

  • copies each data member via their individual copy constructors.

Contact: Compiler-Generated Copy Constructor

In the case of our Contact class, the implicitly generated copy constructor would behave as if it had been written this way.

Contact::Contact (const Contact& a)
  : theName(a.theName), theAddress(a.theAddress)
{}

  • This is, in fact, a perfectly good copy function for this class, so we might as well use the compiler-generated version.

If our data members do not have explicit copy constructors (and their data members do not have explicit copy constructors, and …) <br> </br> then the compiler-provided copy constructor amounts to a bit-by-bit copy.

When to trust the compiler's copy constructor?

Suppose that MailingList provides no explicit copy constructor:

MailingList x = y;

This is not good. Note that adding & removing contacts from one mailinglist will affect the other.

Preferred Copy

What we really wanted, after the copy: MailingList x = y;

Shallow vs Deep Copy

Copy operations are distinguished by how they treat pointers:

  • In a shallow copy, all pointers are copied.

    • Leads to shared data on the heap.

  • In a deep copy, objects pointed to are copied, then the new pointer set to the address of the copied object.

    • Copied objects keep exclusive access to the things they point to.

MailingList with Deep Copy

MailingList::MailingList(const MailingList& ml)
  : first(NULL), last(NULL), theSize(0)
{
  for (ML_Node* current = ml.first; current != NULL;
       current = current->next)
    addContact(current->contact);
}

Shallow copy is wrong when...

  • Your ADT has pointers among its data members, and

  • You don't want to share the objects being pointed to.

Compiler-generated copy constrcutors are wrong when...

  • Your ADT has pointers among its data members, and

  • You don't want to share the objects being pointed to.

Assignment

When we write mlist1 = mlist2, that's shorthand for either mlist1.operator=(mlist2) or operator=(mlist1,mlist2), depending on whether operator= has been declared as a member function of the MailingList class or as an ordinary, stand-alone function.

Compiler-Generated Assignment Ops

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

  • assigns each data member in turn.

  • If none of the members have programmer-supplied assignment ops, then this is a shallow copy

Contact: Compiler-Generated Assignment

For example, we have not provided an assignment operator for the Contact class. Therefore the compiler will attempt to generate one, just as if we had written

class Contact {
public:
  <[:]>
<[+]>
  Contact& operator= (const Contact&);<[-]>
     <[:]>

Contact: Compiler-Generated Asst

The automatically generated body for this assignment operator will be the equivalent of

Contact& Contact::operator= (const Contact& a)
{
  theName = a.theName;
  theAddress = a.theAddress;
  return *this;
}

And that automatically generated assignment is just fine for Contact.

Return values in Asst Ops

Contact& Contact::operator= (const Contact& a)
{
  theName = a.theName;
  theAddress = a.theAddress;
  return *this;
}

The return value returns the value just assigned, allowing programmers to chain assignments together:

con3 = con2 = con1;

This can simplify code where a computed value needs to be tested and then maybe used again if it passes the test, e.g.,

while ((x = foo(y)) > 0) {
  do_something_useful_with(x);
}

When to trust the compiler's asst operator?

Assume that MailingList does not provide an explicit assignment op:

x = y;

Asst: MailingList with Deep Copy

class MailingList
{
public:
  MailingList();
  MailingList(const MailingList&);
  ~MailingList();

  const MailingList& operator= (const MailingList&);
    <[:]>
};


const MailingList& MailingList::operator= (const MailingList& ml)
{
  if (this != &ml)
    {
      clear();
      for (ML_Node* current = ml.first; current != NULL;
	   current = current->next)
	addContact(current->contact);
    }
  return *this;
}

void MailingList::clear()
{
  ML_Node* current = first;
  while (current != NULL)
    {
      ML_Node* next = current->next;
      delete current;
      current = next;
    }
  first = last = NULL;
  theSize = 0;
}

Shallow copy is wrong when...

  • Your ADT has pointers among its data members, and

  • You don't want to share the objects being pointed to.

Compiler-generated assignment ops are wrong when...

  • Your ADT has pointers among its data members, and

  • You don't want to share the objects being pointed to.

  • The automatically generated destructor simply invokes the destructors for any data member objects.

  • If none of the members have programmer-supplied destructors, does nothing.

MailingList::~MailingList()
{
  clear();
}

MailingList should not be left with the compiler-generated destructor.

  • Because the MailingList addContact functions allocate a linked list node for each contact

  • We need to make sure that the memory used for these nodes gets recovered

Compiler-generated destructors are wrong when...

  • Your ADT has pointers among its data members, and

  • You need to recover the allocated storage when the ADT object (and the pointer) are being destroyed

    • Usually because you aren't sharing the objects being pointed to.

The Rule of the Big 3

The rule of the big 3 states that,

if you provide your own version of any one of the big 3, you should provide your own version of all 3.

Why? Because we don't trust the compiler-generated…

  • copy constructor if our data members include pointers to data we don't share

  • assignment operator if our data members include pointers to data we don't share

  • destructor if our data members include pointers to data we don't share

So if we don't trust one, we don't trust any of them.


In the Forum:

(no threads at this time)