Follow Techotopia on Twitter

On-line Guides
All Guides
eBook Store
iOS / Android
Linux for Beginners
Office Productivity
Linux Installation
Linux Security
Linux Utilities
Linux Virtualization
Linux Kernel
System/Network Admin
Programming
Scripting Languages
Development Tools
Web Development
GUI Toolkits/Desktop
Databases
Mail Systems
openSolaris
Eclipse Documentation
Techotopia.com
Virtuatopia.com
Answertopia.com

How To Guides
Virtualization
General System Admin
Linux Security
Linux Filesystems
Web Servers
Graphics & Desktop
PC Hardware
Windows
Problem Solutions
Privacy Policy

  




 

 

Thinking in C++
Prev Contents / Index Next

Copy-construction

The problem occurs because the compiler makes an assumption about how to create a new object from an existing object. When you pass an object by value, you create a new object, the passed object inside the function frame, from an existing object, the original object outside the function frame. This is also often true when returning an object from a function. In the expression

HowMany h2 = f(h);

h2, a previously unconstructed object, is created from the return value of f( ), so again a new object is created from an existing one.

The compiler’s assumption is that you want to perform this creation using a bitcopy, and in many cases this may work fine, but in HowMany it doesn’t fly because the meaning of initialization goes beyond simply copying. Another common example occurs if the class contains pointers – what do they point to, and should you copy them or should they be connected to some new piece of memory?

Fortunately, you can intervene in this process and prevent the compiler from doing a bitcopy. You do this by defining your own function to be used whenever the compiler needs to make a new object from an existing object. Logically enough, you’re making a new object, so this function is a constructor, and also logically enough, the single argument to this constructor has to do with the object you’re constructing from. But that object can’t be passed into the constructor by value because you’re trying to define the function that handles passing by value, and syntactically it doesn’t make sense to pass a pointer because, after all, you’re creating the new object from an existing object. Here, references come to the rescue, so you take the reference of the source object. This function is called the copy-constructor and is often referred to as X(X&), which is its appearance for a class called X.

If you create a copy-constructor, the compiler will not perform a bitcopy when creating a new object from an existing one. It will always call your copy-constructor. So, if you don’t create a copy-constructor, the compiler will do something sensible, but you have the choice of taking over complete control of the process.

Now it’s possible to fix the problem in HowMany.cpp:

//: C11:HowMany2.cpp
// The copy-constructor
#include <fstream>
#include <string>
using namespace std;
ofstream out("HowMany2.out");

class HowMany2 {
  string name; // Object identifier
  static int objectCount;
public:
  HowMany2(const string& id = "") : name(id) {
    ++objectCount;
    print("HowMany2()");
  }
  ~HowMany2() {
    --objectCount;
    print("~HowMany2()");
  }
  // The copy-constructor:
  HowMany2(const HowMany2& h) : name(h.name) {
    name += " copy";
    ++objectCount;
    print("HowMany2(const HowMany2&)");
  }
  void print(const string& msg = "") const {
    if(msg.size() != 0) 
      out << msg << endl;
    out << '\t' << name << ": "
        << "objectCount = "
        << objectCount << endl;
  }
};

int HowMany2::objectCount = 0;

// Pass and return BY VALUE:
HowMany2 f(HowMany2 x) {
  x.print("x argument inside f()");
  out << "Returning from f()" << endl;
  return x;
}

int main() {
  HowMany2 h("h");
  out << "Entering f()" << endl;
  HowMany2 h2 = f(h);
  h2.print("h2 after call to f()");
  out << "Call f(), no return value" << endl;
  f(h);
  out << "After call to f()" << endl;
} ///:~

There are a number of new twists thrown in here so you can get a better idea of what’s happening. First, the string name acts as an object identifier when information about that object is printed. In the constructor, you can put an identifier string (usually the name of the object) that is copied to name using the string constructor. The default = "" creates an empty string. The constructor increments the objectCount as before, and the destructor decrements it.

Next is the copy-constructor, HowMany2(const HowMany2&). The copy-constructor can create a new object only from an existing one, so the existing object’s name is copied to name, followed by the word “copy” so you can see where it came from. If you look closely, you’ll see that the call name(h.name) in the constructor initializer list is actually calling the string copy-constructor.

Inside the copy-constructor, the object count is incremented just as it is inside the normal constructor. This means you’ll now get an accurate object count when passing and returning by value.

The print( ) function has been modified to print out a message, the object identifier, and the object count. It must now access the name data of a particular object, so it can no longer be a static member function.

Inside main( ), you can see that a second call to f( ) has been added. However, this call uses the common C approach of ignoring the return value. But now that you know how the value is returned (that is, code inside the function handles the return process, putting the result in a destination whose address is passed as a hidden argument), you might wonder what happens when the return value is ignored. The output of the program will throw some illumination on this.

Before showing the output, here’s a little program that uses iostreams to add line numbers to any file:

//: C11:Linenum.cpp
//{T} Linenum.cpp
// Add line numbers
#include "../require.h"
#include <vector>
#include <string>
#include <fstream>
#include <iostream>
#include <cmath>
using namespace std;

int main(int argc, char* argv[]) {
  requireArgs(argc, 1, "Usage: linenum file\n"
    "Adds line numbers to file");
  ifstream in(argv[1]);
  assure(in, argv[1]);
  string line;
  vector<string> lines;
  while(getline(in, line)) // Read in entire file
    lines.push_back(line);
  if(lines.size() == 0) return 0;
  int num = 0;
  // Number of lines in file determines width:
  const int width = 
    int(log10((double)lines.size())) + 1;
  for(int i = 0; i < lines.size(); i++) {
    cout.setf(ios::right, ios::adjustfield);
    cout.width(width);
    cout << ++num << ") " << lines[i] << endl;
  }
} ///:~

The entire file is read into a vector<string>, using the same code that you’ve seen earlier in the book. When printing the line numbers, we’d like all the lines to be aligned with each other, and this requires adjusting for the number of lines in the file so that the width allowed for the line numbers is consistent. We can easily determine the number of lines using vector::size( ), but what we really need to know is whether there are more than 10 lines, 100 lines, 1,000 lines, etc. If you take the logarithm, base 10, of the number of lines in the file, truncate it to an int and add one to the value, you’ll find out the maximum width that your line count will be.

You’ll notice a couple of strange calls inside the for loop: setf( ) and width( ). These are ostream calls that allow you to control, in this case, the justification and width of the output. However, they must be called each time a line is output and that is why they are inside the for loop. Volume 2 of this book has an entire chapter explaining iostreams that will tell you more about these calls as well as other ways to control iostreams.

When Linenum.cpp is applied to HowMany2.out, the result is

 1) HowMany2()
 2)   h: objectCount = 1
 3) Entering f()
 4) HowMany2(const HowMany2&)
 5)   h copy: objectCount = 2
 6) x argument inside f()
 7)   h copy: objectCount = 2
 8) Returning from f()
 9) HowMany2(const HowMany2&)
10)   h copy copy: objectCount = 3
11) ~HowMany2()
12)   h copy: objectCount = 2
13) h2 after call to f()
14)   h copy copy: objectCount = 2
15) Call f(), no return value
16) HowMany2(const HowMany2&)
17)   h copy: objectCount = 3
18) x argument inside f()
19)   h copy: objectCount = 3
20) Returning from f()
21) HowMany2(const HowMany2&)
22)   h copy copy: objectCount = 4
23) ~HowMany2()
24)   h copy: objectCount = 3
25) ~HowMany2()
26)   h copy copy: objectCount = 2
27) After call to f()
28) ~HowMany2()
29)   h copy copy: objectCount = 1
30) ~HowMany2()
31)   h: objectCount = 0

As you would expect, the first thing that happens is that the normal constructor is called for h, which increments the object count to one. But then, as f( ) is entered, the copy-constructor is quietly called by the compiler to perform the pass-by-value. A new object is created, which is the copy of h (thus the name “h copy”) inside the function frame of f( ), so the object count becomes two, courtesy of the copy-constructor.

Line eight indicates the beginning of the return from f( ). But before the local variable “h copy” can be destroyed (it goes out of scope at the end of the function), it must be copied into the return value, which happens to be h2. A previously unconstructed object (h2) is created from an existing object (the local variable inside f( )), so of course the copy-constructor is used again in line nine. Now the name becomes “h copy copy” for h2’s identifier because it’s being copied from the copy that is the local object inside f( ). After the object is returned, but before the function ends, the object count becomes temporarily three, but then the local object “h copy” is destroyed. After the call to f( ) completes in line 13, there are only two objects, h and h2, and you can see that h2 did indeed end up as “h copy copy.”

Thinking in C++
Prev Contents / Index Next

 
 
   Reproduced courtesy of Bruce Eckel, MindView, Inc. Design by Interspire