Copy Constructor, Assignment Operator, Destructor

There are three methods that are more important than all other methods in a class. They're so important, that you should think of them as a group: the "three amigos" of C++, if you will.

In fact, they're so important that if you don't implement these methods, the compiler will generate implicit versions of these methods. Let's repeat that one more time: "if you don't implement these methods, the compiler will generate implicit versions of these methods". Some folks think that that if you don't write a copy constructor, then you can't copy. That's incorrect. The compiler generates implicit versions.

Let's list out the behavior of the implicit versions.

When to Write?

There are two rules of thumb when it comes to writing these three methods.

  1. Write all three, or don't write any of them.
  2. Prefer not to write these methods if the implicit ones are correct.
Why should you prefer not to write these methods? Two reasons. You can make mistakes, and it can waste time. Even if you don't make mistakes, or can fix them, you may waste an hour or two trying to get it right, when there's no need to do it.

That raises the question: when should you write these methods?

Shallow vs. Deep Copy

Recall that if you don't write the three methods, you get implicit versions. If you have pointers which point to dynamically allocated memory, then the implicit versions create shallow copies. That is, they copy pointers, not the objects.

Sometimes, this is the behavior you want. However, other times, it's not the behavior you want. In fact, many times, it's not the behavior you want. For example, suppose you have variables x and y. They both contain linked lists. You copy y into x. Now both objects contain the "same" list. If you modify x, do you expect y to change? Probably not. If you have a shallow copy, then changing x will change y and vice versa.

When you need a deep copy of dynamic memory allocated objects, then you must write your own copy constructor, assignment operator, and destructor.

What About Strings?

Suppose your class contains a data member whose type is string, which is the C++ style string. This is an object. More than likely, this object uses dynamic memory allocation. Should you write all three methods? You want deep copies, don't you?

Yet, the answer is no. You don't need to, if it's an object. When a data member is an object (as opposed to a pointer to an object), then it's assumed the object handles its own memory allocation through its own copy constructor, assignment operator, and destructor.

You don't allocate memory for the string using "new", so you don't have to worry about deallocating using "delete" or copying it.

In fact, this is the behavior we want out of objects. When you make an object a data member, you really shouldn't have to worry about whether it has dynamic memory allocation or not. If you had to manage the memory of strings, then that would simply make programming that much more difficult. An object should take care of itself. If it doesn't, it hasn't been correctly implemented.

Why Write All Three?

Whenever a class has to allocate and deallocate memory, most programmers would agree that writing a destructor is necessary. But what happens if the copy constructor and assignment operator is not written? Bad things, that's what.

Here's a classic example of what happens. You have an object called x of type Foo. Assume x has a data member which is a pointer to a dynamically allocated array. You pass x to a function by value.

Normally, you wouldn't pass by value since it's far more efficient to pass by either reference or const reference (which only have to copy pointers). However, it's not that uncommon to pass by value (for example, lots of people pass C++ strings by value).

In any case, when an object is passed by value, the copy constructor is called. Since the copy constructor wasn't implemented, then the implicit version is used. If you refer to the list earlier on, you will recall that when an implicit copy constructor is called on pointers, the address is copied.

This is called a shallow copy. If the parameter is y, then y is a shallow copy of x, which means that y's pointer points to the same dynamic memory that x's does. Both point to the same dynamic array.

The copy constructor is called when the argument is passed. However, notice that there was no call to "new". Thus, the object is created without an explicit call to the constructor. Thus, there's no explicit call to the destructor. The destructor will get called when the object leaves the function scope.

Since you wrote the destructor, this will call the "delete" on the pointer in y. But recall that this is the same pointer as in x since a shallow copy was made. Thus, the array is deleted, and x now points to garbage memory.

Worse, the user usually doesn't suspect this will happen. They either get a mysterious core dump later on, or wonder why the data is mysteriously changing, before core dumping.

Implementing All Three

When you implement all three, it may be quite convenient to write two helper functions. In fact, we insist that you do. We'll explain why in a moment.

These are the helper functions you should write:

Let's consider an example of how they should be written.

class Array {
  int size;
  int *arr; // dyn alloc
public:
  ...
};
Here are all three helper methods.
void Array::init() {
   arr = NULL; 
   size = 0;
}

// Note: NO deallocation
void Array::copy( const Array & other ) {
   size = other.size;
   if ( size > 0 ) 
    {
       arr = new int[ other.size ];
       for ( int i = 0; i < size; i++ )
          arr[ i ] = other.arr[ i ];
    } 
   else
    arr = NULL;
}

void Array::freeMem() {
   delete [] arr;
}
The last two methods are the important one. init() is something of a frill. Once you have these methods, you can implement the three methods without thinking too hard.
// default
void Array::Array() {
   init();
}

// copy constructor
void Array::Array( const Array & other ) {
   init();
   copy( other );
}

// assignment operator
Array & Array::operator=( const Array & other ) {
   if ( this != &other ) {
     freeMem();
     init(); 
     copy( other );
   }
   return *this;
}
By using helper functions, you can see the difference between the copy constructor and assignment operator more clearly. In particular, a copy constructor doesn't free any memory, while the assignment operator does.

Also, the assignment operator checks for self assignment, while the copy constructor does not (since the object is just being constructed, it can hardly be said to exist, so how could one copy it). Furthermore, the assignment operator deallocates memory, and returns a pointer to the "this" object.

Actually, the init() part is not nearly as silly as I said before. In fact, it may be more important than you expect. When attempting to copy, often, you need to make sure the data members have been initialized to something reasonable. This is particularly true when trying to copy linked lists. You get core dumps if the data members are not initialized, when copying linked lists, since the pointer is garbage.

Pitfalls

Among the most common idea for implementing a copy constructor is the following:
// copy constructor
void Array::Array( const Array & other ) {
   *this = other;  // calling op=   AVOID
}
Some people think: "the overloaded operator = is so easy to write, I will use that". It's not a bad idea, but recall the differences. With a copy constructor, you know that the object is being constructed for the first time, so there is no memory to be deallocated. With an assignment operator, the object has been in existence for a while, and therefore may deallocate memory. Thus, the assignment operator does both deallocation and copying while the copy constructor only copies.

For the copy constructor to call the assignment operator, this suggests that you want memory to be deallocated and then copied, which suggests a misunderstanding.

There's also the potential for an infinite loop. This occurs when you accidentally make the return type of the assignment operator an object, rather than an object reference.

By returning an object, you are returning by value. This calls the copy constructor, which calls the assignment operator, which cal;s the copy constructor, etc.

The following are other pitfalls.

Web Accessibility