The
copy-constructor
Now
that you understand the basics of the reference in C++, you’re ready to
tackle one of the more confusing concepts in the language: the copy-constructor,
often called
X(X&)
(“X of X ref”). This constructor is essential to control passing
and returning of user-defined types by value during function calls.
Passing
& returning by value
To
understand the need for the copy-constructor, consider the way C handles
passing and returning variables by value during
function calls. If you declare a function and make a function call,
int f(int x, char c);
int g = f(a, b);
how
does the compiler know how to pass and return those variables? It just knows!
The range of the types it must deal with is so small –
char,
int,
float,
and
double
and their variations – that this information is built into the compiler.
If
you figure out how to generate assembly code with your compiler and determine
the statements generated by the function call to
f( ),
you’ll get the equivalent of,
push b
push a
call f()
add sp,4
mov g, register a
This
code has been cleaned up significantly to make it generic – the
expressions for
b
and
a
will be different depending on whether the variables are global (in which case
they will be
_b
and
_a)
or local (the compiler will index them off the stack pointer). This is also
true for the expression for
g.
The appearance of the call to
f( )
will depend on your name-mangling scheme, and “register a” depends
on how the CPU registers are named within your assembler. The logic behind the
code, however, will remain the same.
In
C and C++, arguments are pushed on the stack from right to left, the function
call is made, then the calling code is responsible for cleaning the arguments
off the stack (which accounts for the
add
sp,4
).
But notice that to pass the arguments by value, the compiler simply pushes
copies on the stack – it knows how big they are and that pushing those
arguments makes accurate copies of them.
The
return value of
f( )
is placed in a register. Again, the compiler knows everything there is to know
about the return value type because it’s built into the language, so the
compiler can return it by placing it in a register. The simple act of copying
the bits of the value is equivalent to copying the object.
Passing
& returning large objects
But
now consider user-defined types. If you create a class and you want to pass an
object of that class by value, how is the compiler supposed to know what to do?
This is no longer a built-in type the compiler writer knows about; it’s a
type someone has created since then.
To
investigate this, you can start with a simple structure that is clearly too
large to return in registers:
//: C11:PassStruct.cpp
// Passing a big structure
struct Big {
char buf[100];
int i;
long d;
} B, B2;
Big bigfun(Big b) {
b.i = 100; // Do something to the argument
return b;
}
int main() {
B2 = bigfun(B);
} ///:~
Decoding
the assembly output is a little more complicated here because most compilers
use “helper” functions rather than putting all functionality
inline. In
main( ),
the call to
bigfun( )
starts as you might guess – the entire contents of
B
is pushed on the stack. (Here, you might see some compilers load registers with
the address of
B
and its size, then call a helper function to
push it onto the stack.)
In
the previous example, pushing the arguments onto the stack was all that was
required before making the function call. In
PassStruct.cpp,
however, you’ll see an additional action: The address of
B2
is pushed before making the call, even though it’s obviously not an
argument. To comprehend what’s going on here, you need to understand the
constraints on the compiler when it’s making a function call.
Function-call
stack frame
When
the compiler generates code for a function call, it first pushes all the
arguments on the stack, then makes the call. Inside the function itself, code
is generated to move the stack pointer down even further to provide storage for
the function’s local variables. (“Down” is relative here;
your machine may increment or decrement the stack pointer during a push.) But
during the assembly-language CALL,
the CPU pushes the address in the program code where the function call
came
from
,
so the assembly-language RETURN can
use that address to return to the calling point. This address is of course
sacred, because without it your program will get completely lost. Here’s
what the stack frame looks like after the CALL and the allocation of local
variable storage in the function:
The
code generated for the rest of the function expects the memory to be laid out
exactly this way, so it can carefully pick from the function arguments and
local variables without touching the return address. I shall call this block of
memory, which is everything used by a function in the process of the function
call, the
function
frame
. You
might think it reasonable to try to return values on the stack. The compiler
could simply push it, and the function could return an offset to indicate how
far down in the stack the return value begins.
Re-entrancy
The
problem occurs because functions in C and C++ support interrupts; that is, the
languages are
re-entrant.
They also support recursive function calls. This means that at any point in the
execution of a program an interrupt can
occur without disturbing the program. Of course, the person who writes the
interrupt service routine (ISR)
is responsible for saving and restoring all the registers he uses, but if the
ISR needs to use any memory that’s further down on the stack, that must
be a safe thing to do. (You can think of an ISR as an ordinary function with no
arguments and
void
return value that saves and restores the CPU state. An ISR function call is
triggered by some hardware event rather than an explicit call from within a
program.)
Now
imagine what would happen if the called function tried to return values on the
stack from an ordinary function. You can’t touch any part of the stack
that’s above the return address, so the function would have to push the
values below the return address. But when the assembly-language RETURN is
executed, the stack pointer must be pointing to the return address (or right
below it, depending on your machine), so right before the RETURN, the function
must move the stack pointer up, thus clearing off all its local variables. If
you’re trying to return values on the stack below the return address, you
become vulnerable at that moment because an interrupt could come along. The ISR
would move the stack pointer down to hold its return address and its local
variables and overwrite your return value.
To
solve this problem, the caller could be responsible for allocating the extra
storage on the stack for the return values
before
calling the function. However, C was not designed this way, and C++ must be
compatible. As you’ll see shortly, the C++ compiler uses a more efficient
scheme.
Your
next idea might be to return the value in some global data area, but this
doesn’t work either. Re-entrancy means that any function can interrupt
any other function,
including
the same function you’re currently inside
.
Thus, if you put the return value in a global area, you might return into the
same function, which would overwrite that return value. The same logic applies
to recursion. The
only safe place to return values is in the registers, so you’re back to
the problem of what to do when the registers aren’t large enough to hold
the return value. The answer is to push the address of the return value’s
destination on the stack as one of the function arguments, and let the function
copy the return information directly into the destination. This not only solves
all the problems, it’s more efficient. It’s also the reason that, in
PassStruct.cpp,
the compiler pushes the address of
B2
before the call to
bigfun( )
in
main( ).
If you look at the assembly output for
bigfun( ),
you can see it expects this hidden argument and performs the copy to the
destination
inside
the function.
Bitcopy
versus initialization
So
far, so good. There’s a workable process for passing and returning large
simple structures. But notice that all you have is a way to copy the bits from
one place to another, which certainly works fine for the primitive way that C
looks at variables. But in C++ objects can be much more sophisticated than a
patch of bits; they have meaning. This meaning may not respond well to having
its bits copied.
Consider
a simple example: a class that knows how many objects of its type exist at any
one time. From Chapter 8, you know the way to do this is by including a
static
data member:
//: C11:HowMany.cpp
// Class counts its objects
#include <fstream>
using namespace std;
ofstream out("HowMany.out");
class HowMany {
static int object_count;
public:
HowMany() {
object_count++;
}
static void print(const char* msg = 0) {
if(msg) out << msg << ": ";
out << "object_count = "
<< object_count << endl;
}
~HowMany() {
object_count--;
print("~HowMany()");
}
};
int HowMany::object_count = 0;
// Pass and return BY VALUE:
HowMany f(HowMany x) {
x.print("x argument inside f()");
return x;
}
int main() {
HowMany h;
HowMany::print("after construction of h");
HowMany h2 = f(h);
HowMany::print("after call to f()");
} ///:~
The
class
HowMany
contains a
static
int
and a
static
member function
print( )
to report the value of that
int,
along with an optional message argument. The constructor increments the count
each time an object is created, and the destructor decrements it.
The
output, however, is not what you would expect:
after construction of h: object_count = 1
x argument inside f(): object_count = 1
~HowMany(): object_count = 0
after call to f(): object_count = 0
~HowMany(): object_count = -1
~HowMany(): object_count = -2
After
h
is created, the object count is one, which is fine. But after the call to
f( )
you
would expect to have an object count of two, because
h2
is now in scope as well. Instead, the count is zero, which indicates something
has gone horribly wrong. This is confirmed by the fact that the two destructors
at the end make the object count go negative, something that should never happen.
Look
at the point inside
f( ),
which occurs after the argument is passed by value. This means the original
object
h
exists outside the function frame, and there’s an additional object
inside
the function frame, which is the copy that has been passed by value. However,
the argument has been passed using C’s primitive notion of bitcopying,
whereas the C++
HowMany
class requires true initialization to maintain its integrity, so the default
bitcopy fails to produce the desired effect.
When
the local object goes out of scope at the end of the call to
f( ),
the destructor is called, which decrements
object_count,
so outside the function,
object_count
is zero. The creation of
h2
is also performed using a bitcopy, so the constructor isn’t called there,
either, and when
h
and
h2
go out of scope, their destructors cause the negative values of
object_count.
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
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 <cstring>
using namespace std;
ofstream out("HowMany2.out");
class HowMany2 {
enum { bufsize = 30 };
char id[bufsize]; // Object identifier
static int object_count;
public:
HowMany2(const char* ID = 0) {
if(ID) strncpy(id, ID, bufsize);
else *id = 0;
++object_count;
print("HowMany2()");
}
// The copy-constructor:
HowMany2(const HowMany2& h) {
strncpy(id, h.id, bufsize);
strncat(id, " copy", bufsize - strlen(id));
++object_count;
print("HowMany2(HowMany2&)");
}
// Can't be static (printing id):
void print(const char* msg = 0) const {
if(msg) out << msg << endl;
out << '\t' << id << ": "
<< "object_count = "
<< object_count << endl;
}
~HowMany2() {
--object_count;
print("~HowMany2()");
}
};
int HowMany2::object_count = 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 character buffer
id
acts as an object identifier so you can figure out which object the information
is being printed about. In the constructor, you can put an identifier string
(usually the name of the object) that is copied to
id
using the Standard C library function
strncpy( ),
which only copies a certain number of characters, preventing overrun of the
buffer.
Next
is the copy-constructor,
HowMany2(HowMany2&).
The copy-constructor can create a new object only from an existing one, so the
existing object’s name is copied to
id,
followed by the word “copy” so you can see where it came from. Note
the use of the Standard C library function
strncat( )
to copy a maximum number of characters into
id,
again to prevent overrunning the end of the buffer.
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
id
data of a particular object, so it can no longer be a
static
member function. Inside
main( ),
you can see 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
// Add line numbers
#include <fstream>
#include <strstream>
#include <cstdlib>
#include "../require.h"
using namespace std;
int main(int argc, char* argv[]) {
requireArgs(argc, 1, "Usage: linenum file\n"
"Adds line numbers to file");
strstream text;
{
ifstream in(argv[1]);
assure(in, argv[1]);
text << in.rdbuf(); // Read in whole file
} // Close file
ofstream out(argv[1]); // Overwrite file
assure(out, argv[1]);
const int bsz = 100;
char buf[bsz];
int line = 0;
while(text.getline(buf, bsz)) {
out.setf(ios::right, ios::adjustfield);
out.width(2);
out << ++line << ") " << buf << endl;
}
} ///:~
The
entire file is read into a
strstream
(which can be both written to and read from) and the
ifstream
is closed with scoping. Then an
ofstream
is created for the same file, overwriting it.
getline( )
fetches a line at a time from the
strstream
and line numbers are added as the line is written back into the file.
The
line numbers are printed right-aligned in a field width of two, so the output
still lines up in its original configuration. You can change the program to add
an optional second command-line argument that allows the user to select a field
width,
or
you can be more clever and count all the lines in the file to determine the
field width automatically.
When
Linenum.cpp
is applied to
HowMany2.out,
the result is
1) HowMany2()
2) h: object_count = 1
3) entering f()
4) HowMany2(HowMany2&)
5) h copy: object_count = 2
6) x argument inside f()
7) h copy: object_count = 2
8) returning from f()
9) HowMany2(HowMany2&)
10) h copy copy: object_count = 3
11) ~HowMany2()
12) h copy: object_count = 2
13) h2 after call to f()
14) h copy copy: object_count = 2
15) call f(), no return value
16) HowMany2(HowMany2&)
17) h copy: object_count = 3
18) x argument inside f()
19) h copy: object_count = 3
20) returning from f()
21) HowMany2(HowMany2&)
22) h copy copy: object_count = 4
23) ~HowMany2()
24) h copy: object_count = 3
25) ~HowMany2()
26) h copy copy: object_count = 2
27) after call to f()
28) ~HowMany2()
29) h copy copy: object_count = 1
30) ~HowMany2()
31) h: object_count = 0
As
you would expect, the first thing that happens is 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.”
Temporary
objects
Line
15 begins the call to
f(h),
this time ignoring the return value. You can see in line 16 that the
copy-constructor is called just as before to pass the argument in. And also, as
before, line 21 shows the copy-constructor is called for the return value. But
the copy-constructor must have an address to work on as its destination (a
this
pointer).
Where is the object returned to?
It
turns out the compiler can create a temporary object whenever it needs one to
properly evaluate an expression. In this case it creates one you don’t
even see to act as the destination for the ignored return value of
f( ).
The lifetime of this temporary object is
as short as possible so the landscape doesn’t get cluttered up with
temporaries waiting to be destroyed, taking up valuable resources. In some
cases, the temporary might be immediately passed to another function, but in
this case it isn’t needed after the function call, so as soon as the
function call ends by calling the destructor for the local object (lines 23 and
24), the temporary object is destroyed (lines 25 and 26).
Now,
in lines 28-31, the
h2
object is destroyed, followed by
h,
and the object count goes correctly back to zero.
Default
copy-constructor
Because
the copy-constructor implements pass and return by value, it’s important
that the compiler will create one for you in the case of simple structures
– effectively, the same thing it does in C. However, all you’ve
seen so far is the default primitive behavior: a bitcopy.
When
more complex types are involved, the C++ compiler will still automatically
create a copy-constructor if you don’t make one. Again, however, a bitcopy doesn’t
make sense, because it doesn’t necessarily implement the proper meaning.
Here’s
an example to show the more intelligent approach the compiler takes. Suppose
you create a new class composed of objects of several existing classes. This is
called, appropriately enough,
composition,
and it’s one of the ways you can make new classes from existing classes.
Now take the role of a naive user who’s trying to solve a problem quickly
by creating the new class this way. You don’t know about
copy-constructors, so you don’t create one. The example demonstrates what
the compiler does while creating the default copy-constructor for your new class:
//: C11:Autocc.cpp
// Automatic copy-constructor
#include <iostream>
#include <cstring>
using namespace std;
class WithCC { // With copy-constructor
public:
// Explicit default constructor required:
WithCC() {}
WithCC(const WithCC&) {
cout << "WithCC(WithCC&)" << endl;
}
};
class WoCC { // Without copy-constructor
enum { bsz = 30 };
char buf[bsz];
public:
WoCC(const char* msg = 0) {
memset(buf, 0, bsz);
if(msg) strncpy(buf, msg, bsz);
}
void print(const char* msg = 0) const {
if(msg) cout << msg << ": ";
cout << buf << endl;
}
};
class Composite {
WithCC withcc; // Embedded objects
WoCC wocc;
public:
Composite() : wocc("Composite()") {}
void print(const char* msg = 0) {
wocc.print(msg);
}
};
int main() {
Composite c;
c.print("contents of c");
cout << "calling Composite copy-constructor"
<< endl;
Composite c2 = c; // Calls copy-constructor
c2.print("contents of c2");
} ///:~
The
class
WithCC
contains a copy-constructor, which simply announces it has been called, and
this brings up an interesting issue. In the class
Composite,
an object of
WithCC
is created using a default constructor. If there were no constructors at all in
WithCC,
the compiler would automatically create a default constructor,
which would do nothing in this case. However, if you add a copy-constructor,
you’ve told the compiler you’re going to handle constructor
creation, so it no longer creates a default constructor for you and will
complain unless you explicitly create a default constructor as was done for
WithCC. The
class
WoCC
has no copy-constructor, but its constructor will store a message in an
internal buffer that can be printed out using
print( ).
This constructor is explicitly called in
Composite’s
constructor
initializer list (briefly introduced in Chapter 6 and covered fully in Chapter
12). The reason for this becomes apparent later.
The
class
Composite
has member objects of both
WithCC
and
WoCC
(note
the embedded object
WOCC
is initialized in the constructor-initializer list, as it must be), and no
explicitly defined copy-constructor. However, in
main( )
an object is created using the copy-constructor in the definition:
The
copy-constructor for
Composite
is
created automatically by the compiler, and the output of the program reveals
how it is created.
To
create a copy-constructor for a class that uses composition (and inheritance,
which is introduced in Chapter 12), the compiler recursively calls the
copy-constructors for all the member objects and base classes. That is, if the
member object also contains another object, its copy-constructor is also
called. So in this case, the compiler calls the copy-constructor for
WithCC.
The output shows this constructor being called. Because
WoCC
has no copy-constructor, the compiler creates one for it, which is the default
behavior of a bitcopy, and calls that inside the
Composite
copy-constructor. The call to
Composite::print( )
in main shows that this happens because the contents of
c2.WOCC
are identical to the contents of
c.WOCC.
The process the compiler goes through to synthesize a copy-constructor is called
memberwise
initialization. It’s
best to always create your own copy-constructor rather than letting the
compiler do it for you. This guarantees it will be under your control.
Alternatives
to copy-construction
At
this point your head may be swimming, and you might be wondering how you could
have possibly written a functional class without knowing about the
copy-constructor. But remember: You need a copy-constructor only if
you’re going to pass an object of your class
by
value
.
If that never happens, you don’t need a copy-constructor.
Preventing
pass-by-value
“But,”
you say, “if I don’t make a copy-constructor, the compiler will
create one for me. So how do I know that an object will never be passed by
value?”
There’s
a simple technique for preventing pass-by-value: Declare a
private
copy-constructor.
You don’t even need to create a definition, unless one of your member
functions or a
friend
function needs to perform a pass-by-value. If the user tries to pass or return
the object by value, the compiler will produce an error message because the
copy-constructor is
private.
It can no longer create a default copy-constructor because you’ve
explicitly stated you’re taking over that job.
//: C11:Stopcc.cpp
// Preventing copy-construction
class NoCC {
int i;
NoCC(const NoCC&); // No definition
public:
NoCC(int ii = 0) : i(ii) {}
};
void f(NoCC);
int main() {
NoCC n;
//! f(n); // Error: copy-constructor called
//! NoCC n2 = n; // Error: c-c called
//! NoCC n3(n); // Error: c-c called
} ///:~
Notice
the use of the more general form
Functions
that modify outside objects
Reference
syntax is nicer to use than pointer syntax, yet it clouds the meaning for the
reader. For example, in the iostreams library one overloaded version of the
get( )
function takes a
char&
as an argument, and the whole point of the function is to modify its argument
by inserting the result of the
get( ).
However, when you read code using this function it’s not immediately
obvious to you the outside object is being modified:
char c;
cin.get(c);
Instead,
the function call looks like a pass-by-value, which suggests the outside object
is
not
modified.
Because
of this, it’s probably safer from a code maintenance standpoint to use
pointers when you’re passing the address of an argument to modify. If you
always
pass addresses as
const
references except
when you intend to modify the outside object via the address, where you pass by
non-
const
pointer, then your code is far easier for the reader to follow.
Go to CodeGuru.com
Contact: webmaster@codeguru.com
© Copyright 1997-1999 CodeGuru