Chapter 17. Copying the Copy Copy Copy Constructor

In This Chapter

  • Introducing the copy constructor

  • Making copies

  • Having copies made for you automatically

  • Creating shallow copies versus deep copies

  • Avoiding all those copies

The constructor is a special function that C++ invokes automatically when an object is created to allow the object to initialize itself. Chapter 15 introduces the concept of the constructor, whereas Chapter 16 describes other types of constructors. This chapter examines a particular variation of the constructor known as the copy constructor.

Copying an Object

A copy constructor is the constructor that C++ uses to make copies of objects. It carries the name X::X(X&), where X is the name of the class. That is, it's the constructor of class X, which takes as its argument a reference to an object of class X. Now, I know that this sounds really useless, but just give me a chance to explain why C++ needs such a beastie.

Why you need the copy constructor

Think for a moment about what happens when you call a function like the following:

void fn(Student fs)
{
    // ...same scenario; different argument...
}
int main(int argcs, char* pArgs[])
{
    Student ms;
    fn(ms);
    return 0;
}

In the call to fn(), C++ passes a copy of the object ms and not the object itself.

Now consider what it means to create a copy of an object. First, it takes a constructor to create an object, even a copy of an existing object. C++ could create a default copy constructor that copies the existing object into the new object one byte at a time. That's what older languages such as C do. But what if the class doesn't want a simple copy of the object? What if something else is required? (Ignore the "why?" for a little while.) The class needs to be able to specify exactly how the copy should be created.

Thus, C++ uses a copy constructor in the preceding example to create a copy of the object ms on the stack during the call of function fn(). This particular copy constructor would be Student::Student(Student&) — say that three times quickly.

Using the copy constructor

The best way to understand how the copy constructor works is to see one in action. Consider the following CopyConstructor program:

//
//  CopyConstructor - demonstrate a copy constructor
//
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;

class Student
{
  public:
    // conventional constructor
    Student(const char *pName = "no name", int ssId = 0)
      : name(pName), id(ssId)
    {
        cout << "Constructed "  << name << endl;
    }

    // copy constructor
    Student(Student& s)
      : name("Copy of " + s.name), id(s.id)
    {
        cout << "Constructed "  << name << endl;
    }

    ~Student()
    {
        cout << "Destructing " << name << endl;
    }

  protected:
    string name;
    int  id;
};

// fn - receives its argument by value
void fn(Student copy)
{
    cout << "In function fn()" << endl;
}

int main(int nNumberofArgs, char* pszArgs[])
{
    Student chester("Chester", 1234);
    cout << "Calling fn()" << endl;
    fn(chester);
    cout << "Back in main()" << endl;

    // wait until user is ready before terminating program
    // to allow the user to see the program results
    system("PAUSE");
    return 0;
}

The output from executing this program appears as follows:

Constructed Chester
Calling fn()
Constructed Copy of Chester
In function fn()
Destructing Copy of Chester
Back in main()
Press any key to continue ...

The normal Student constructor generates the first message from the declaration on the first line of main() about creating chester. main() then outputs the Calling... message before calling fn(). As part of the function call process, C++ invokes the copy constructor to make a copy of chester to pass to fn(). The copy constructor outputs a message. The function fn() outputs the In function... message. The copied Student object copy is destructed at the return from fn(). The original object, chester, is destructed at the end of main().

Tip

The assignment operator is defined on the string class to concatenate two strings or a string and a null-terminated character string. You see how to define operators on user-defined classes in Chapter 22.

The copy constructor copies the name of the object provided it is prepended with "Copy of" into its own name field. It then copies the id field from s into the id field of the current object. The constructor outputs the resulting name field before returning.

The first line of output shows the chester object being created. The third line demonstrates the copy Student being generated from the copy constructor in the call to fn(). The function fn() does nothing more than output a message. The copy is destructed as part of the return, which generates the destructing... message.

The Automatic Copy Constructor

Like the default constructor, the copy constructor is important; important enough that C++ thinks no class should be without one. If you don't provide your own copy constructor, C++ generates one for you. (This differs from the default constructor that C++ provides unless your class has constructors defined for it.)

The copy constructor provided by C++ performs a member-by-member copy of each data member. The copy constructor that early versions of C++ provided performed a bit-wise copy. The difference is that a member-by-member copy invokes all copy constructors that might exist for the members of the class, whereas a bit-wise copy does not. You can see the effects of this difference in the following DefaultCopyConstructor sample program. (I left out the definition of the Student class to save space — it's identical to that shown in the CopyConstructor program. The entire DefaultCopyConstructor program is on the enclosed CD-ROM.)

class Tutor
{
  public:
    Tutor(Student& s)
       : student(s), id(0)
    {
        cout << "Constructing Tutor object" << endl;
  }
  protected:
    Student student;
    int id;
};

void fn(Tutor tutor)
{
    cout << "In function fn()" << endl;
}

int main(int argcs, char* pArgs[])
{
    Student chester("Chester");
    Tutor tutor(chester);
    cout << "Calling fn()" << endl;
    fn(tutor);
    cout << "Back in main()" << endl;

    // wait until user is ready before terminating program
    // to allow the user to see the program results
    system("PAUSE");
    return 0;
}

Executing this program generates the following output:

Constructed Chester
Constructed Copy of Chester
Constructing Tutor object
Calling fn()
Constructed Copy of Copy of Chester
In function fn()
Destructing Copy of Copy of Chester
Back in main()
Press any key to continue...

Constructing the chester object generates the first output message from the "plain Jane" constructor. The constructor for the tutor object invokes the Student copy constructor to generate its own student data member and then outputs its own message. This accounts for the next two lines of output.

The program then passes a copy of the Tutor object to the function fn(). Because the Tutor class does not define a copy constructor, the program invokes the default copy constructor to make a copy to pass to fn().

The default Tutor copy constructor invokes the copy constructor for each data member. The copy constructor for int does nothing more than copy the value. You've already seen how the Student copy constructor works. This is what generates the Constructed Copy of Copy of Chester message. The destructor for the copy is invoked as part of the return from function fn().

Creating Shallow Copies versus Deep Copies

Performing a member-by-member copy seems the obvious thing to do in a copy constructor. Other than adding the capability to tack on silly things such as "Copy of " to the front of students' names, when would you ever want to do anything but a member-by-member copy?

Consider what happens if the constructor allocates an asset, such as memory off the heap. If the copy constructor simply makes a copy of that asset without allocating its own asset, you end up with a troublesome situation: two objects thinking they have exclusive access to the same asset. This becomes nastier when the destructor is invoked for both objects and they both try to put the same asset back. To make this more concrete, consider the following example class:

//
//  ShallowCopy - performing a byte-by-byte (shallow) copy
//                is not correct when the class holds
          assets
//
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;
class Person
 {
   public:
     Person(const char *pN)
     {
         cout << "Constructing " << pN << endl;
         pName = new string(pN);
     }
     ~Person()
     {
        cout << "Destructing " << pName
             << " (" << *pName << ")" << endl;
        *pName = "already destructed memory";
        // delete pName;
     }
  protected:
     string *pName;
 };

 void fn()
 {
      // create a new object
      Person p1("This_is_a_very_long_name");

      // copy the contents of p1 into p2
      Person p2(p1);
 }

 int main(int argcs, char* pArgs[])
 {
     cout << "Calling fn()" << endl;
     fn();
     cout << "Back in main()" << endl;

     // wait until user is ready before terminating program
     // to allow the user to see the program results
     system("PAUSE");
     return 0;
 }

This program generates the following output:

Calling fn()
Constructing This_is_a_very_long_name
Destructing 0x360f78 (This_is_a_very_long_name)
Destructing 0x360f78 (already destructed memory)
Back in main()
Press any key to continue...

The constructor for Person allocates memory off the heap to store the person's name. The destructor would normally return this memory to the heap using the delete keyword; however, in this case, I've replace the call to delete with a statement that replaces the name with a message. The main program calls the function fn(), which creates one person, p1, and then makes a copy of that person, p2. Both objects are destructed automatically when the program returns from the function.

Only one constructor output message appears when this program is executed. That's not too surprising because the C++ provided copy constructor used to build p2 performs no output. The Person destructor is invoked twice, however, as both p1 and p2 go out of scope. The first destructor outputs the expected This_is_a_very_long_name. The second destructor indicates that the memory has already been deleted. Notice also that the address of the memory block is the same for both objects (0x360F78).

Warning

If the program really were to delete the name, the program would become unstable after the second delete and might not even complete properly without crashing.

The problem is shown graphically in Figure 17-1. The object p1 is copied into the new object p2, but the assets are not. Thus, p1 and p2 end up pointing to the same assets (in this case, heap memory). This is known as a shallow copy because it just "skims the surface," copying the members themselves.

Shallow copy of p1 to p2.

Figure 17.1. Shallow copy of p1 to p2.

The solution to this problem is demonstrated visually in Figure 17-2. This figure represents a copy constructor that allocates its own assets to the new object.

Deep copy of p1 to p2.

Figure 17.2. Deep copy of p1 to p2.

The following shows an appropriate copy constructor for class Person, the type you've seen up until now. (This class is embodied in the program DeepCopy, which is on this book's CD-ROM.)

class Person
{
  public:
    Person(const char *pN)
    {
        cout << "Constructing " << pN << endl;
        pName = new string(pN);
    }
    Person(Person& person)
    {
        cout << "Copying " << *(person.pName) << endl;
        pName = new string(*person.pName);
    }
    ~Person()
    {
        cout << "Destructing " << pName
             << " (" << *pName << ")" << endl;
        *pName = "already destructed memory";
        // delete pName;
    }
 protected:
    string *pName;
};

Here you see that the copy constructor allocates its own memory block for the name and then copies the contents of the source object name into this new name block. This is a situation similar to that shown in Figure 17-2. Deep copy is so-named because it reaches down and copies all the assets. (Okay, the analogy is pretty strained, but that's what they call it.)

The output from this program is as follows:

Calling fn()
Constructing This_is_a_very_long_name
Copying This_is_a_very_long_name
Destructing 0x270fb0 (This_is_a_very_long_name)
Destructing 0x270f60 (This_is_a_very_long_name)
Back in main()
Press any key to continue...

The destructor for Person now indicates that the string pointers in p1 and p2 don't point to the same block of memory: the addresses of the two objects are different, and the name in the version owned by the copy has not been overwritten indicating that it's been deleted.

It's a Long Way to Temporaries

Passing arguments by value to functions is the most obvious but not the only example of the use of the copy constructor. C++ creates a copy of an object under other conditions as well.

Consider a function that returns an object by value. In this case, C++ must create a copy using the copy constructor. This situation is demonstrated in the following code snippet:

Student fn();        // returns object by value
int main(int argcs, char* pArgs[])
{
  Student s;
  s = fn();         // call to fn() creates temporary

  // how long does the temporary returned by fn()last?
  return 0;
}

The function fn() returns an object by value. Eventually, the returned object is copied to s, but where does it reside until then?

C++ creates a temporary object into which it stuffs the returned object. "Okay," you say. "C++ creates the temporary, but how does it know when to destruct it?" Good question. In this example, it doesn't make much difference, because you'll be through with the temporary when the copy constructor copies it into s. But what if s is defined as a reference? It makes a big difference how long temporaries live because refS exists for the entire function:

int main(int argcs, char* pArgs[])
{
    Student& refS = fn();
    // ...now what?...
    return 0;
}

Temporaries created by the compiler are valid throughout the extended expression in which they were created and no further.

In the following function, I mark the point at which the temporary is no longer valid:

Student fn1();
int fn2(Student&);
int main(int argcs, char* pArgs[])
{
    int x;
    // create a Student object by calling fn1().
    // Pass that object to the function fn2().
    // fn2() returns an integer that is used in some
    // silly calculation.
    // All this time the temporary returned from fn1()
    // remains valid.
    x = 3 * fn2(fn1()) + 10;

    // the temporary returned from fn1() is now no longer
           valid
    // ...other stuff...
    return 0;
}

This makes the reference example invalid because the object may go away before refS does, leaving refS referring to a nonobject.

Avoiding temporaries, permanently

It may have occurred to you that all this copying of objects hither and yon can be a bit time-consuming. What if you don't want to make copies of everything? The most straightforward solution is to pass objects to functions and return objects from functions by reference. Doing so avoids the majority of temporaries.

But what if you're still not convinced that C++ isn't out there craftily constructing temporaries that you know nothing about? Or what if your class allocates unique assets that you don't want copied? What do you do then?

You can add an output statement to your copy constructor. The presence of this message when you execute the program warns you that a copy has just been made.

A more crafty approach is to declare the copy constructor protected, as follows:

class Student
{
  protected:
    Student(Student&s){}

  public:
    // ...everything else normal...
};

Note

The C++ '09 standard also allows the programmer to delete the copy constructor:

class Student
{
    Student(Student&s) = delete;

    // ...everything else normal...
};

Either declaring the copy constructor protected or deleting it entirely precludes any external functions, including C++, from constructing a copy of your Student objects. If no one can invoke the copy constructor, no copies are being generated. Voilà.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.12.76.164