dcsimg

Demystifying Copy Constructors in C++

WEBINAR:
On-Demand

Desktop-as-a-Service Designed for Any Cloud ? Nutanix Frame


As the name suggests, a copy constructor is typically used to create a copy of an object. There are many intricate details about how it operates and how it is used. It also helps in control passing and returning of user-defined types by value during function calls. This article gets into some of the conceptual details about copy constructor with appropriate examples in C++.

The Constructor

According to the Object Oriented Programming principle, when an object of a class is instantiated, its data members should be initialized properly during creation. This is important because using an object with uninitialized data members makes these members prone to unintended problems. Initialization is done with the help of a constructor, a special member function defined as the same name as the class name. This naming scheme makes it distinct from the other member functions of the class. Unlike member functions, another distinctive feature is that a constructor never returns a value, not even void. And, constructors usually are declared as public.

C++ relies on the constructor call to create an object. This ensures that objects are created and data members are initialized appropriately before being used. The constructor call is implicit and, if we explicitly do not create a constructor, the C++ compiler provides one for us, called the default constructor. Default constructors are no-argument constructors; therefore, if we want to send any argument in the constructor, we must do so with an explicit definition. But, note that, as we define an explicit constructor in a class, the C++ compiler stops providing a default constructor. Here is an example of its uses:

class IntArray
{
private:
   int* p2arr;
   int max;
   int count;
public:
   IntArray(int n = 256);
   IntArray(int n, int val);
   // ...
};
// Constructors are invoked when we create objects
IntArray a1;                     // Invokes no-arg constructor
IntArray a2(5,7);                // Invokes constructor with
                                 // 2-argument
IntArray *cp = new IntArray();   // Dynamic object creation

Copy Constructor

Copy constructors are those which take a reference of themselves and create a copy. For example, we can create an object: object (const object&) and refer to as "object of object ref". It also can essentially be used in control passing and returning of user-defined types by value during a function call. According to Lipman, "A constructor is a copy constructor if its first parameter is a reference to the class type and any additional parameters have default values". The simplest reason of its existence is that we want to create a copy of the object itself.

There are a couple of things to note while creating a copy constructor.

  • The reference arguments of the copy constructor ideally will be designated as const to allow a const object to be copied.
  • The argument of the copy constructor must be by reference and not by value because, if you imagine an argument by value, it is nothing but a copy of the object itself. As a result, the copy constructor would itself invoke the copy constructor recursively. This is a fatal logic. The argument by reference, on the other hand, is not a copy; therefore, there is no question of invoking the copy constructor. This is the reason why argument of the copy constructor is by reference and not by value.
class IntArray
{
private:
   // ...
public:
   // ...
   IntArray(const IntArray&);
   // ...
};

Note that, if we do not provide any explicit copy constructor, the C++ compiler provides a default one. But, we must be very careful in relying on the default copy constructor. Sometimes, it does exactly what we do not want, especially with dynamic initialization.

Default Copy Constructor

Suppose a class does not have an explicit copy constructor. Then, what the compiler does is that it creates a minimal version of the same. This is called a default or standard copy constructor. What it does is simply copy data members of the object passed to it to the corresponding data members of the new object. A typically standard copy constructor may be sufficient, but in some cases, a simple copying of values is not exactly what we want, especially when the object contains dynamic members (pointers). This means that it would copy the pointer content, which is an address to a location, and not the values contained in the address. As a result, data pointers from two distinct object points to the same memory location when standard copy constructor is invoked. This is definitely not what we want and is trouble uncalled for. What we want is two identical but distinct objects but what we get is a dynamic member (pointer) in two separate objects pointing to the same memory location. Now, imagine what happens if we try to release the memory of one object. The data member (pointer) of the second object would then point to a memory location which no longer exists because we have already deleted the first object. This is where we must intervene and override the default copy constructor.

A Quick Example

#ifndef INTARRAY_H
#define INTARRAY_H

class IntArray
{
private:
   int* p2arr;
   int max;
   int count;
public:
   IntArray(int n = 256);
   IntArray(int n, int val);
   IntArray(const IntArray&);
   ~IntArray();
   inline int length() const {return count;}
   int& operator[](int i);
   int operator[](int i) const;
   bool add(int val);
   bool remove(int pos);
};
#endif   // INTARRAY_H

Listing 1: IntArray class declaration

#include "intarray.h"
#include <iostream>
using namespace std;
IntArray::IntArray(int n)
{
   max = n;
   count = 0;
   p2arr = new int[max];
}

IntArray::IntArray(int n, int val)
{
   max = count = n;
   p2arr = new int[max];
   for(int i=0;i<max;i++)
      p2arr[i] = val;
}

IntArray::IntArray(const IntArray& ar)
{
   max = ar.max;
   count = ar.count;
   p2arr = new int[max];
   for(int i=0;i<max;i++)
      p2arr[i] = ar.p2arr[i];
}

IntArray::~IntArray()
{
   delete[] p2arr;
}
int &IntArray::operator[](int i)
{
   if(i<0||i>=count){
      cerr<<"\n array out of range!\n";
      exit(1);
   }
   return p2arr[i];
}

int IntArray::operator[](int i) const
{
   if(i<0||i>=count){
      cerr<<"\n array out of range!\n";
      exit(1);
   }
   return p2arr[i];
}

bool IntArray::add(int val)
{
   bool flag = false;
   if(count<max){
      p2arr[count++] = val;
      flag = true;
   }
   return flag;
}

bool IntArray::remove(int pos)
{
   bool flag = false;
   if(pos>=0 && pos<count){
      for(int i=pos;i<count-1;i++)
         p2arr[i] = p2arr[i+1];
      --count;
      flag = true;
   }
   return flag;
}

Listing 2: IntArray class member function definition

#include <iostream>
#include "intarray.h"

using namespace std;

int main()
{
   IntArray a(5, 10);
   a[2] = 20;
   IntArray ca(a);
   for( int i=0; i < ca.length(); i++)
   cout << ca[i]<<endl;

   return 0;
}

Listing 3: Testing IntArray object

Therefore, it is understood that we must write a new copy constructor, especially in classes which have dynamic data members to ensure that when a copy constructor is invoked, it copies the content of the location that dynamic members point to distinctively rather than the address of the content.

Private Copy Construction

Relying on default copy construction is fine in most cases. If the situation discussed above never occurs, we do not need an explicit copy constructor. But, how do we ensure that an object will never be passed by value? The technique is to declare a private copy constructor. Then, we do not need to create a definition except if we want any member function or friend function to do pass by value. This technique restricts the user to pass or return an object by value. The compiler flags an error if we try to do so.

#ifndef MYCLASS_H
#define MYCLASS_H

class MyClass
{

private:
   int info;
   MyClass(const MyClass&);
public:
   MyClass(int i);
};

#endif   // MYCLASS_H


#include "myclass.h"
MyClass::MyClass(int i)
{
   info = i;
}

#include "myclass.h"

using namespace std;

void func(MyClass c);

int main()
{
   MyClass m(10);
   /*
    * None of the following is allowed.

      func(m);
      MyClass m1 = m;
      MyClass(m);

    */
   return 0;
}

Conclusion

Note that these are not comprehensive, but are some of the intricate concepts associated with copy constructor in C++. To sum up,

  • A copy constructor takes a reference to an existing object of its type to create a new one.
  • A copy constructor is automatically invoked by the compiler whenever we pass or return an object by value.
  • We can restrict an object passed or return by value using a private copy constructor.
  • A default copy constructor is fine, but be cautious if there is a dynamic data member.

Happy learning :)



About the Author

Manoj Debnath

manosolireap@outlook.com

Related Articles

Most Popular Programming Stories

More for Developers

RSS Feeds

Thanks for your registration, follow us on our social networks to keep up-to-date