Copy Constructor and the Assignment Operator

In this article I discuss the purpose served by the copy constructor, the occasions where it is required, when should a developer provide a custom definition. There is also a discussion of the assignment operator and its overloading. Also discussed are copy elision, and the empirical rule of three.

Last Reviewed and Updated on February 7, 2020
Posted by Parveen(Hoven),
Aptitude Trainer and Software Developer

My C/C++ Videos on Youtube

Here is the complete playlist for video lectures and tutorials for the absolute beginners. The language has been kept simple so that anybody can easily understand them. I have avoided complex jargon in these videos.

Copy constructor and assignment operators are two related concepts in C++. Recently, move constructor and move assignment have also been added, but in this tutorial we shall be discussing only the assignment and copy constructors.

What is a Copy Constructor?

As the name suggests, it is a special function that is called by the compiler when a new object is created from an existing object. The important point here is that a brand new object is being created - but it is being created from an already existing object - not from raw materials directly. It is like lighting a candle with the help of another candle that is already lit. When a new object is created from an existing object, the compiler doesn't call any of the constructors - default or parameterized - that we have seen and used so far. Here's an example code that you can run and see. Even though there are two objects objA, and objB, there is only one call to the parametrized constructor.

class CMyClass
{

    int i;

public:
    CMyClass(int x) : i(x)
    {

        cout << "c-tor called!" << endl;

    }

    void print()
    {

        cout << "i = " << i << endl;

    }

};

int main()
{

    CMyClass objA(9);

    CMyClass objB = objA;

    objA.print();

    objB.print();

    return 0;

}

// output is
c-tor called!
i = 9
i = 9

The object objB is also properly initialized. It doesn't contain any random data. It is not a coincidence that it's i is having the same value as that of the object objA. How did that happen? Where did that initializing constructor come from?

There is a special constructor that comes into play when a new object is created from an existing object. This constructor is called the copy constructor. If you do not write a copy constructor, then the compiler adds it for you. Unlike a default constructor, the body of the compiler generated copy constructor is not empty. It adds code for member-wise copy of data members. If the class has an int i member then the copy constructor will add code to copy the i of existing object to the i of the new object being created. The copy constructor of a class T has the form T(const T&). It accepts a const reference to its own type. We can add a copy constructor to our class CMyClass like shown below.

class CMyClass
{

    int i;

public:
    CMyClass(int x) : i(x)
    {

        cout << "copy c-tor called!" << endl;

    }

    CMyClass(const CMyClass& x)
    {

        cout << "c-tor called!" << endl;

    }

    void print()
    {

        cout << "i = " << i << endl;

    }

};

int main()
{

    CMyClass objA(9);

    CMyClass objB = objA;

    objA.print();

    objB.print();

    return 0;

}

// output
copy c-tor called!
c-tor called!
i = 9
i = -858993460

From the output of the above code we can see that there is a call to copy constructor. The total number of objects is two, and so are the number of constructor calls. But this time, the i of objB is showing junk value, because we didn't write the code for initialization of i during copy. This problem is corrected by modifying the copy constructor as shown below.

class CMyClass
{

    int i;

public:
    CMyClass(int x) : i(x)
    {

        cout << "c-tor called!" << endl;

    }

    CMyClass(const CMyClass& x)
    {

        // initialization of
        // data members
        this->i = x.i;

        cout << "copy c-tor called!" << endl;

    }

    void print()
    {

        cout << "i = " << i << endl;

    }

};

int main()
{

    CMyClass objA(9);

    CMyClass objB = objA;

    objA.print();

    objB.print();

    return 0;

}

// output
c-tor called!
copy c-tor called!
i = 9
i = 9

When is the Copy Constructor used?

There are three occasions when a copy construction is required.

  1. Explicit Copy: We have already discussed this use above. When a new object is created from an existing object, the copy constructor is used.
  2. Pass by Value: When a class object is passed by value to another function, then the copy constructor is invoked to copy the argument into the parameter of the function. In the code below the object is objA is being copied into the parameter ox with the help of the copy constructor.
    void fx(CMyClass ox)
    {
    
        ox.print();
    
    }
    
    int main()
    {
    
        CMyClass objA(9);
    
        // objA is copies into ox
        // the copy ctor will be called
        fx(objA);
    
        return 0;
    
    }
    
    // output
    c-tor called!
    copy c-tor called!
    i = 9
    
  3. Return by Value: The third place where the copy constructor is required is when a function returns a class object by value. In the code below the function fx returns a class object by value. When the function returns it copies the returned value into objA.
    CMyClass fx()
    {
    
        CMyClass ox(9);
    
        return ox;
    
    }
    
    int main()
    {
    
        CMyClass objA = fx();
    
        return 0;
    
    }
    
    //output
    c-tor called!
    copy c-tor called!
    

Purpose of the Copy Constructor?

Why does C++ provide for copy constructors? The purpose is to support "copy". If the compiler creates just the default constructor, then how would we copy one object into another? There is no mechanism in the default constructor that allows the copy of data members. But, in the first place, the default constructor takes no arguments, so how would the RHS object be "passed in" for initialization?

It might seem that the compiler provided copy constructor is sufficient, because it can effectively do the simple thing - copy data members from an existing object to the new object. But this is a "blind" copy; it is a copy of values, of contents of memory. Things can go wrong when addresses are copied that way. Let's have a look at a class which holds an int* address.

class CMyClass
{

    int *ip;

public:
    CMyClass(int x) : ip(new int(x))
    {

        cout << "ctor called !\n";

    }

public:
    ~CMyClass()
    {

        delete ip;

        ip = 0;

        cout << "dtor called, memory released\n";

    }

};

int main()
{

    CMyClass objA(9);

    return 0;

}


This class contains an int* as its data member. Heap allocation is done in the constructor, and the memory is released in the destructor. What happens when a copy is created? The compiler generated copy constructor copies the contents of the data members only. So when a copy is created, the address of the heap allocated object is copied. The address is "blindly" copied, without creating a new int. So both the original object and it's copy point to the same memory - they are not two independent objects. They share the common data member. This means the compiler generated copy constructor doesn't create an exact copy, in the sense we expect. The following code will throw an error at run time. Why?

class CMyClass
{

    int *ip;

public:
    CMyClass(int x) : ip(new int(x))
    {

        cout << "ctor called !\n";

    }

public:
    ~CMyClass()
    {

        delete ip;

        ip = 0;

        cout << "dtor called, memory released\n";

    }

};

int main()
{

    CMyClass objA(9);

    CMyClass objB = objA;

    return 0;

}


In the above code, the int* of objB contains a copy of the int* of objA. This means they point to the same memory. Now, when the destructor of objA releases that object, the address contained by int* of objB becomes invalid. And, when the destructor of objB executes, an error is thrown because deletion of a non-zero, invalid address is an error.

The above problem can be corrected by providing your own custom copy constructor. Instead of simply copying the address, a new object is created on the heap, but it's given the same value. In this way, we have a copy of an existing object. This new object is an independent object that contains its own data, the value of which matches the value of the existing object. This is the completed code.

class CMyClass
{

    int *ip;

public:
    CMyClass(int x) : ip(new int(x))
    {

        cout << "ctor called !\n";

    }

    CMyClass(CMyClass& cmc)
    {

        ip = new int(*cmc.ip);

        cout << "Copy ctor called!\n";

    }

public:
    ~CMyClass()
    {

        delete ip;

        ip = 0;

        cout << "dtor called, memory released\n";

    }

};

int main()
{

    CMyClass objA(9);

    CMyClass objB = objA;

    return 0;

}


const T& vs T&

The copy constructor can take either a const or a non-const argument. Both are acceptable, but the standard one is the one with the const reference. The following code shows the standard version of the copy constructor.

class CMyClass
{

public:
    CMyClass()
    {

    }

public:
    CMyClass(const CMyClass& cmc)
    {

        cout << "Copy ctor called!\n";

    }

};


The copy constructor with the const reference promises that it won't alter the right side object. This is very logical because it would make no sense to modify an object that's going to be used to create another copy. Sometimes it might be necessary to modify the right hand side object, for example, if it is going to contain a counter to keep track of the number of copies created from that object. In those cases the non-const version of the copy constructor could be used, or perhaps, the counter variable could be marked mutable.

A copy constructor is compulsory if you would be copying from a temporary object because temporaries are constant. They can't be passed as an argument to a function that doesn't take a const parameter. In the following code a copy is being created from the temporary variable created by the return from fx. This code should result in a compiler error.

class CMyClass
{

public:
    CMyClass()
    {

        cout << "Default ctor called!\n";

    }

public:
    CMyClass(CMyClass& cmc)
    {

        cout << "Copy ctor called!\n";

    }

};

CMyClass fx()
{

    CMyClass cmc1;

    return cmc;

}

int main()
{

    // COMPILER ERROR,
    CMyClass objA = fx();

    return 0;

}


Copy Elision

Sometimes a compiler can skip creation of copies and objects in certain situations. It can be done for code optimization. Consider the following code. Here the class CMyClass has both a default constructor, and a copy constructor. There is a global function that returns an object of CMyClass. When an object of the class CMyClass is created inside main, in the manner as shown below, then we would expect the copy constructor to be called, but an optimizing compiler, skips an unnecessary copy and directly creates an object through the default constructor. Run this code to see the output. But I would mention here that copy elision is not that simple as it appears. A very detailed discussion of this concept is beyond the scope of this article.

class CMyClass
{

public:
    CMyClass()
    {

        cout << "default ctor called!\n";

    }

    CMyClass(const CMyClass& cmc)
    {

        cout << "copy ctor called!\n";

    }

};

CMyClass fx()
{

    return CMyClass();

}

int main ()
{

    CMyClass objA(fx());

    return 0;

}


Preventing Copy

It is possible to prevent an object from being copied at all. One way is to make the copy constructor private. Another is to delete the copy constructor. This latter feature is available in C++11.

class CMyClass
{

private:
    CMyClass(CMyClass&);

    // OR
    CMyClass(CMyClass&) = delete;

};


Assignment Operator

The assignment operator is used copy an existing object into another existing object. In this case both the objects are already existing, so the purpose is to make their data members same.

class CMyClass
{

    int i;

public:
    CMyClass(int x) : i(x)
    {

    }

    void print()
    {

        cout << this->i << endl;

    }

};

int main ()
{

    CMyClass objA(9), objB(2);

    cout << "Values before assignment\n";

    objA.print();

    objB.print();

    // assignment
    objA = objB;

    cout << "Values after assignment\n";

    objA.print();

    objB.print();

    return 0;

}


Just like the copy constructor is provided by the compiler, an assignment operator is also provided by the compiler, if you do not provide a definition for it. The assignment operator simply does a member wise copy, in the same way as the copy constructor does.

If your class hosts pointers and those pointers are dynamically initialized to heap objects, then member-wise copy is not sufficient. In that case the assignment operator must be provided a custom definition. Here's how it can be done.

class CMyClass
{

    int *pi;

public:
    CMyClass(int x) : pi(new int(x))
    {

    }

    void print()
    {

        cout << *pi << endl;

    }

public:
    CMyClass& operator=(const CMyClass& cmc)
    {

        // prevent self assignment
        if(this != &cmc)
        {

            *this->pi = *cmc.pi;

        }

        return *this;

    }

public:
    ~CMyClass()
    {

        delete pi;

        pi = 0;

    }

};

int main ()
{

    CMyClass objA(9), objB(2);

    cout << "Values before assignment\n";

    objA.print();

    objB.print();

    // assignment
    objA = objB;

    cout << "Values after assignment\n";

    objA.print();

    objB.print();

    return 0;

}


One interesting observation can be made now. When do we provide a destructor? It is compulsory if we want automatic deletion of heap allocated memory, of heap allocated pointers. And, as we have seen above, whenever we have pointers as data members, we have to provide custom definitions for copy constructor, and also for the assignment operator. This brings us to an empirical rule - the rule of three - which states that if your class implements any one of the three - the destructor, the copy constructor or the assignment operator - then it also has to implement the other two. In other words, if you are implementing one of these three, then your class is having pointers as data members and these pointers hold addresses of objects on the heap. If the latter is the case, then, the other two implementations are compulsory as well.

Compiler Synthesized Functions

At this stage I must put the list of the functions that a compiler can write for us. It writes a default constructor, if we do not provide any other constructor. It provides a copy constructor if we do not write our copy constructor. It also provides an assignment operator if we do not write one. Lastly, it writes a destructor for us, if we haven't written one. Please note that if you provide your copy constructor, then the compiler doesn't provide the default constructor. The converse is not true - i.e., if you provide a default constructor, then the compiler will still provide a copy constructor, if you haven't provided yours.



Creative Commons License
This Blog Post/Article "Copy Constructor and the Assignment Operator" by Parveen (Hoven) is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
Updated on 2020-02-07. Published on: 2015-12-22