Friday, Oct 20th

Last update12:59:40 PM GMT

Copy Constructor and Assignment Operator in C++

Write e-mail

In a F2F interview, with your C++ under test, its certain you will be questioned on Copy Constructor and Assignment Operator. So lets face it. I assume here that the reader is having a fair amount of idea on the use of constructors and destructors. We will still brush up with a few basics.

argaiv1874

Brushing up with the Basics:

We know that if we do not provide any constructor, the compiler provides a default zero-argument constructor (but only if we have not provided any other constructor).

But when do we need to write a destructor? Doesn’t compiler provide it as well if we dont? Yes it does, but if we have allocated some memory in the constructor, then we must write our own destructor to make sure the memory allocated in constructor is freed when the object is destructed like in below case:

class Table {
size_t sz ;
public:
Table(size_t s = 15 ) { p = new Name [sz = s];}//constructor
~Table() { delete [] p ; } //destructor
};

A matching constructor/destructor pair is the usual mechanism for implementing the notion of a variably sized object in C++.

Even for the default constructor there is a catch. If the class contains any constants or reference members, then in that case it becomes mandatory to write a default constructor to initialize them because the one provided by the compiler cannot do this. If we don’t explicitly define our own constructor in this case, compilation will not succeed.

class X
{
private:
int i;
const int a;
int& ref ;
public:
somefunc();
}
X x_obj; //error: no default constructor for X

So we must explicitly define our constructor:

X::X()
{
a = 10;
ref = a;
}

Copying an Object:

Now consider the below class definition:

class Table {
size_t sz ;
Name* nm;
};
//Note: here I have not provided any constructor/destructor.
 

If t1 and t2 are objects of a class Table, t2 =t1 by default means a member wise copy of t1 into t2. Having assignment interpreted this way can cause a surprising (and usually undesired) effect when used on objects of a class with pointer members. Member-wise copy is usually the wrong semantics for copying objects containing resources managed by a constructor/destructor pair. For example:

void h()
{
Table t1 ;
Table t2 = t1 ; // copy initialization: trouble
Table t3 ;
t3 = t2 ; // copy assignment: trouble
}

Here, the Table default constructor is called twice: once each for t1 and t3. It is not called for t2 because that variable was initialized by copying. However, the Table destructor is called three times: once each for t1, t2, and t3! The default interpretation of assignment is member wise copy, so t1 , t2 , and t3 will, at the end of h (), each object will have its Name pointer (Name* nm) pointing to the same memory block that was allocated on the free store when t1 was created.

No pointer to the array of Name is allocated when t3 was created remains because it was overwritten by the t 3 =t 2 assignment.

The array created for t1 appears in t1, t2, and t3, hence, when respective destructors are called, it will be deleted thrice. The result of that is undefined and probably disastrous.

Writing on our own Copy Constructor and Assignment Operator

Such anomalies like above occur when we leave the task of defining what copying an object means to the compiler. The compiler does this through its default copy constructor and default assignment operator, which as we said before are implemented as member wise copy of one object to other.

The default versions fail or give undesirable behavior when the class contains pointer members.

In such cases we should ourselves define what it means to copy an object by writing our own copy constructor and assignment operator.

For the above class this can be done as follows:

class Table
{
public:
//…
Table (const Table&); //copy constructor
Table& = operator (const Table&); //assignment operator
}

Now we can put in any suitable implementation for the two:

Table :: Table (const Table& t)
{
sz = t.sz;
nm = new Name [sz];
for (int i=0; i<sz; i++)
{
nm[i] = t.nm[i];
}
Table& Table :: operator = (const Table& t)
{
if(this != &t)                    //check against self assignment
{
delete[] nm;
nm = new Name [sz];
for (int i=0; i<sz; i++)
nm[i] = t.nm[i];
}
return *this;
}

As is almost always the case, the copy constructor and the copy assignment differ considerably.

The fundamental reason is that a copy constructor initializes uninitialized memory, whereas the copy assignment operator must correctly deal with a well constructed object.

Assignment can be optimized in some cases, but the general strategy for an assignment operator is simple: protect against self assignment, delete old elements, initialize, and copy in new elements. Usually every non-static member must be copied.

The Rule of Three:

There is a very famous Rule of Thumb prevalent in C++ known as the Rule of Three. According to this rule:

If a class defines one of the following it should probably explicitly define all three:

  • destructor
  • copy constructor
  • copy assignment operator

These three functions are special member functions, if one of these functions is used without first being declared by the programmer it will be implicitly implemented by the compiler with the default semantics of doing the said operation on all the members of the class.

The Rule of Three claims that if one of these had to be defined by the programmer, it means that the compiler-generated version does not fit the needs of the class in one case and it will probably not fit in the other cases either. If you revisit our initial discussion, you will realize that this Rule is actually true. If you don’t think so, then read again. :-)

Share this post



Add comment

Please refrain from using slang or abusive language in the comments.
To avoid waiting for your comment to be published after being reviewed, please log in as a registered user.


Security code
Refresh

Web Hosting