Not Copying in C++

So, in our last post we had a look at copy construction and copy assignment. Let’s use the code from that post and look at a quick example. Suppose we have simple function that returns a Bond. Something like:

static Bond BondFactory(double rate)
{
        return Bond(rate);
}

Of course this example is very silly, but let’s just go with it. So, suppose we run the following code in a Main method:

Bond b(BondFactory(1.2));

What do we see? Well, the output should look like this:

Constructing

This seems kind of strange. We constructed a Bond object inside the method BondFactory, and we see in the output that we visited the Bond constructor. However, we also copied the return value of BondFactory into a new Bond object b. So why did execution not pass through the copy constructor?

The answer is copy elision. The constructor sees that we are directly copying the result of the BondFactory method into another Bond object. So, rather than constructing and copying, it just constructs the Bond object once, creating the object b directly. What’s surprising here is that our copy constructor has a side effect, it prints to the console, yet the compiler stills skips it.

Now, suppose we defined a subclass of Bond:

class NamedBond : public Bond
{
private:
       string Name;
public:
       NamedBond(string name, double rate) : Bond(bond), Name(name)
       {
             std::cout << "Sub class constructing" << std::endl;
       }
}

And we had a factory method like:

static Bond BondFactory(double rate)
{
      return new NamedBond(rate, "none");
}

Now, if we run the following code in our main method,

Bond b(BondFactory(1.2));

We will see:

Constructing
Sub class constructing
Copy constructing

This time we do pass through the copy constructor. That is because copy elision only happens when the return type is the same as the value it is being copied into. In this case, the return type is a subclass of the value it is being coped into. So the compiler will not elide the copy step.

We will see the same effect when we copy from temporary objects that never get assigned a name. If we run the following code:

Bond b(Bond(1.2));

Our output will be:

Constructing

Whereas, if we run:

Bond b(NamedBond(1.2, "Name"));

we will get:

Constructing
Sub class constructing
Copy constructing

Similarly if we define an addition operator on Bond objects. If we run the following code:

Bond a(1.6);
Bond b(2.3);
Bond c(a + b);

The output will be:

Constructing
Constructing
Constructing

However, if we use the subtype, as so:

NamedBond a(1.6, "Name1");
NamedBond b(2.3, "Name2");
Bond c(a + b);

We will see the copy construction taking place.

Constructing
sub class Constructing
Constructing
sub class Constructing
Constructing
sub class Constructing
Copy constructing

With copy assignment the constructor is a lot stricter. With our example code, the following:

Bond b(1.2);
Bond c(2.3);
b = c;

will result in:

Constructing
Constructing
Copy assigning

So, the copy assignment method gets called here. This is because the compiler only elides the copy assignment when doing so has no consequence. In our case, if it was elided, we would miss the cout statement so it does not. It makes sense to be stricter with elision for the assignment operator, as we may have logic in that method that frees up resources that have already been initialised, this would not be the case with the copy constructor.

The C++ standard allows copy elision to take place, it does not mandate it. This means that elision behaviour depends entirely on what C++ compiler you use, what version of the language you are using and what compiler settings you have enabled. What I have described above is what you should normally expect to see.

Copy elision can also happen when throwing exceptions. However I will not get into the details here, as it is really not as interesting. If your exceptions are defined in such away that copy elision is important, you are defining them wrong.

Copying in C++

We are going to talk about two special C++ methods, the copy constructor and the copy assignment operator. You will often hear about these in the context of the rule of three or the rule of five. We’re not going to cover either of these rules. Instead we’re going to focus on these two methods themselves. What are the copy constructor and assignment operator for? Why are there two different methods that seem to do the same thing? When do they get used?

The copy constructor is defined just like a normal constructor, except it takes a single argument, a constant reference to an object of that class. The copy assignment operator is defined as an overload of the = operator. Let’s define a trivial class called Bond, that has a single field, a constructor, a copy constructor and a copy assignment operator.

#include <iostream>

class Bond
{
private:
	double Rate;
public:
	Bond(double rate) : Rate(rate)
	{
                std::cout << "Constructing" << std::endl;
	}

        // copy assignment
       	Bond& operator=(const Bond& other)
	{
		std::cout << "Copy assigning" << std::endl;
		Rate = other.Rate;
		return *this;
	}

        // copy constructor
       	Bond(const Bond& other) : Rate(other.Rate)
	{
		std::cout << "Copy constructing" << std::endl;
	}
};

Suppose we run the following code:

Bond A(1.6);
Bond B(A);

You will see, “Constructing” printed to the terminal, followed by “Copy constructing”. Nothing surprising here, when we create Bond A, we use the normal constructor. However, when we create Bond B, we use the copy constructor.

Similarly, if we run the following code from a main method:

Bond A(1.6);
Bond B(1.2);
B = A;

This time we will see our “Constructing” message appear twice followed by “Copy Assigning”. That is exactly what you would expect, as we are clearly using the assignment operator to copy from the Bond A to the Bond B. Now you might think that the copy assignment method is used every time we use the = operator to copy Bond objects. However it is not! Let’s say we ran the following code from a main method:

Bond A(1.6);
Bond B = A;

You won’t see the “Copy Assigning” output, you will see the “Copy constructing” message instead! That’s because we are not copy assigning here. Copy assignment happens whenever we use the = operator to assign to a Bond object that has already been initialised. Whereas, the copy constructor is called when we copy a Bond object into a new Bond that has not been initialised yet.

In our second example, the Bond B was already initialised with the normal constructor, before we assigned to it with =, so the copy assignment method was called. however, in our third example, the Bond B did not exist yet when we used the = operator, so the copy constructor was called. This difference is easy to remember, because constructors always initialise new objects!

When you see an example like this, you might ask yourself a couple of questions. Why are we splitting hairs over these two different cases? Why do we even need two different methods, couldn’t C++ just use a single copy method here?

Well, with these kind of trivial examples, it is a little pointless to have a separate copy constructor and copy assignment operator. So let’s imagine a slightly more complex example. Suppose we have a class, Holder that has a pointer to some object on the heap. Now when copy construct we know that our Holder object is uninitialised, so we can just copy the from source to the target. Whether we do a deep or shallow copy is irrelevant here. When we do a copy assignment, the target object will have a pointer to an object on the heap. So, before we can copy from the source object, we must free this memory with a delete. Otherwise we would end up with a serious memory leak.

To consider an even more complicated example. imagine a client class that talks to a server. When we copy a client object, we may want different behaviour depending on whether the target object has already started talking to a server or not.

So, in more complicated examples, the difference between copy assignment and copy construction can be quite important. The thing that you need to remember is that copy assignment happens when the target is already initialised, copy construction when it has not been initialised yet.