|
|
|
|
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.”
|
|
|