Upcasting
Earlier
in the chapter, you saw how an object of a class derived from
ofstream
has all the characteristics and behaviors of an
ofstream
object. In
FName2.cpp,
any
ofstream
member
function could be called for an
FName2
object.
The
most important aspect of inheritance is not that it provides member functions
for the new class, however. It’s the relationship expressed between the
new class and the base class. This relationship can be summarized by saying,
“The new class
is
a type of
the existing class.”
This
description is not just a fanciful way of explaining inheritance –
it’s supported directly by the compiler. As an example, consider a base
class called
Instrument
that represents musical instruments and a derived class called
Wind.
Because inheritance means that all the functions in the base class are also
available in the derived class, any message you can send to the base class can
also be sent to the derived class. So if the
Instrument
class has a
play( )
member function, so will
Wind
instruments. This means we can accurately say that a
Wind
object is also a type of
Instrument.
The following example shows how the compiler supports this notion:
//: C14:Wind.cpp
// Inheritance & upcasting
enum note { middleC, Csharp, Cflat }; // Etc.
class Instrument {
public:
void play(note) const {}
};
// Wind objects are Instruments
// because they have the same interface:
class Wind : public Instrument {};
void tune(Instrument& i) {
// ...
i.play(middleC);
}
int main() {
Wind flute;
tune(flute); // Upcasting
} ///:~
What’s
interesting in this example is the
tune( )
function, which accepts an
Instrument
reference. However, in
main( )
the
tune( )
function is called by giving it a
Wind
object. Given that C++ is very particular about type checking, it seems strange
that a function that accepts one type will readily accept another type, until
you realize that a
Wind
object is also an
Instrument
object, and there’s no function that
tune( )
could call for an
Instrument
that isn’t also in
Wind.
Inside
tune( ),
the code works for
Instrument
and anything derived from
Instrument,
and the act of converting a
Wind
object, reference, or pointer into an
Instrument
object, reference, or pointer is called
upcasting.
Why
“upcasting”?
The
reason for the term is historical and is based on the way class inheritance
diagrams have
traditionally been drawn: with the root at the top of the page, growing
downward. (Of course, you can draw your diagrams any way you find helpful.) The
inheritance diagram for
Wind.cpp
is then:
Casting
from derived to base moves
up
on the inheritance diagram, so it’s commonly referred to as upcasting.
Upcasting is always safe because you’re going from a more specific type
to a more general type – the only thing that can occur to the class
interface is that it can lose member functions, not gain them. This is why the
compiler allows upcasting without any explicit casts or other special notation.
Downcasting
You
can also perform the reverse of upcasting, called
downcasting,
but this involves a dilemma that is the subject of Chapter 17.
Upcasting
and the copy-constructor (not indexed)
If
you allow the compiler to synthesize a copy-constructor for a derived class, it
will automatically call the base-class copy-constructor, and then the
copy-constructors for all the member objects (or perform a bitcopy on built-in
types) so you’ll get the right behavior:
//: C14:Ccright.cpp
// Correctly synthesizing the CC
#include <iostream>
using namespace std;
class Parent {
int i;
public:
Parent(int ii) : i(ii) {
cout << "Parent(int ii)\n";
}
Parent(const Parent& b) : i(b.i) {
cout << "Parent(Parent&)\n";
}
Parent() :i(0) { cout << "Parent()\n"; }
friend ostream&
operator<<(ostream& os, const Parent& b) {
return os << "Parent: " << b.i << endl;
}
};
class Member {
int i;
public:
Member(int ii) : i(ii) {
cout << "Member(int ii)\n";
}
Member(const Member& m) : i(m.i) {
cout << "Member(Member&)\n";
}
friend ostream&
operator<<(ostream& os, const Member& m) {
return os << "Member: " << m.i << endl;
}
};
class Child : public Parent {
int i;
Member m;
public:
Child(int ii) : Parent(ii), i(ii), m(ii) {
cout << "Child(int ii)\n";
}
friend ostream&
operator<<(ostream& os, const Child& d){
return os << (Parent&)d << d.m
<< "Child: " << d.i << endl;
}
};
int main() {
Child d(2);
cout << "calling copy-constructor: " << endl;
Child d2 = d; // Calls copy-constructor
cout << "values in d2:\n" << d2;
} ///:~
The
operator<<
for
Child
is interesting because of the way that it calls the
operator<<
for the
Parent
part within it: by casting the
Child
object
to a
Parent&
(if you cast to a
Parent
object instead of a reference you’ll end up creating a temporary):
return
os << (Parent&)d << d.m
Since
the compiler then sees it as a
Parent,
it calls the
Parent
version of
operator<<. You
can see that
Child
has no explicitly-defined copy-constructor. The compiler then synthesizes the
copy-constructor (since that is one of the four functions it will synthesize,
along with the default constructor – if you don’t create any
constructors – the
operator=
and the destructor) by calling the
Parent
copy-constructor and the
Member
copy-constructor. This is shown in the output
Parent(int ii)
Member(int ii)
Child(int ii)
calling copy-constructor:
Parent(Parent&)
Member(Member&)
values in d2:
Parent: 2
Member: 2
Child: 2
However,
if you try to write your own copy-constructor for
Child
and
you make an innocent mistake and do it badly:
Child(const
Child& d) : i(d.i), m(d.m) {}
The
default
constructor will be automatically called, since that’s what the compiler
falls back on when it has no other choice of constructor to call (remember that
some
constructor must always be called for every object, regardless of whether
it’s a subobject of another class). The output will then be:
Parent(int ii)
Member(int ii)
Child(int ii)
calling copy-constructor:
Parent()
Member(Member&)
values in d2:
Parent: 0
Member: 2
Child: 2
This
is probably not what you expect, since generally you’ll want the
base-class portion to be copied from the existing object to the new object as
part of copy-construction.
To
repair the problem you must remember to properly call the base-class
copy-constructor (as the compiler does) whenever you write your own
copy-constructor. This can seem a little strange-looking at first but
it’s another example of upcasting:
Child(const Child& d)
: Parent(d), i(d.i), m(d.m) {
cout << "Child(Child&)\n";
}
The
strange part is where the
Parent
copy-constructor is called:
Parent(d).
What does it mean to pass a
Child
object to a
Parent
constructor? Here’s the trick:
Child
is inherited from
Parent,
so a
Child
reference
is
a
Parent
reference. So the base-class copy-constructor upcasts a reference to
Child
to a reference to
Parent
and uses it to perform the copy-construction. When you write your own copy
constructors you’ll generally want to do this.
Composition
vs. inheritance (revisited)
One
of the clearest ways to determine whether you should be using composition or
inheritance is by asking whether you’ll ever need to upcast from your new
class. Earlier in this chapter, the
Stack
class was specialized using inheritance. However, chances are the
Stringlist
objects will be used only as
String
containers, and never upcast, so a more appropriate alternative is composition:
//: C14:Inhstak2.cpp
//{L} ../C13/Stack11
// Composition vs. inheritance
#include <iostream>
#include <fstream>
#include <string>
#include "../require.h"
#include "../C13/Stack11.h"
using namespace std;
class StringList {
Stack stack; // Embed instead of inherit
public:
void push(string* str) {
stack.push(str);
}
string* peek() const {
return (string*)stack.peek();
}
string* pop() {
return (string*)stack.pop();
}
};
int main() {
ifstream file("Inhstak2.cpp");
assure(file, "Inhstak2.cpp");
string line;
StringList textlines;
while(getline(file,line))
textlines.push(new string(line));
string* s;
while((s = textlines.pop()) != 0) // No cast!
cout << *s << endl;
} ///:~
The
file is identical to
Inhstack.cpp,
except that a
Stack
object is embedded in
Stringlist,
and member functions are called for the embedded object. There’s still no
time or space overhead because the subobject takes up the same amount of space,
and all the additional type checking happens at compile time.
You
can also use
private
inheritance to express “implemented in terms of.” The method you
use to create the
Stringlist
class is not critical in this situation – they all solve the problem
adequately. One place it becomes important, however, is when multiple inheritance
might be warranted. In that case, if you can detect a class where composition
can be used instead of inheritance, you may be able to eliminate the need for
multiple inheritance.
Pointer
& reference upcasting
In
Wind.cpp,
the upcasting occurs during the function call – a
Wind
object outside the function has its reference taken and becomes an
Instrument
reference inside the function. Upcasting can also occur during a simple
assignment to a pointer or reference:
Wind w;
Instrument* ip = &w; // Upcast
Instrument& ir = w; // Upcast
Like
the function call, neither of these cases require an explicit cast.
A
crisis
Of
course, any upcast loses type information about an object. If you say
Wind w;
Instrument* ip = &w;
the
compiler can deal with
ip
only
as an
Instrument
pointer and nothing else. That is, it cannot know that
ip
actually
happens to point to a
Wind
object. So when you call the
play( )
member function by saying
the
compiler can know only that it’s calling
play( )
for an
Instrument
pointer, and call the base-class version of
Instrument::play( )
instead of what it should do, which is call
Wind::play( ).
Thus you won’t get the correct behavior.
This
is a significant problem; it is solved in the next chapter by introducing the
third cornerstone of object-oriented programming: polymorphism (implemented in
C++ with
virtual
functions).
Go to CodeGuru.com
Contact: webmaster@codeguru.com
© Copyright 1997-1999 CodeGuru