|
|||||
14:
Inheritance &
Composition
One
of the most compelling
features about C++ is
code
reuse. But to be revolutionary, you
need to be
able
to do a lot more than copy
code and change
it.
613
That's
the C approach, and it hasn't worked very
well. As with
most
everything in C++, the
solution revolves around the
class.
You
reuse code by creating new
classes, but instead of
creating
them
from scratch, you use existing classes
that someone else
has
built
and debugged.
The
trick is to use the classes
without soiling the existing
code. In
this
chapter you'll see two ways to accomplish
this. The first is
quite
straightforward: You simply create
objects of your existing
class
inside the new class. This
is called composition
because
the new
class
is composed of objects of existing
classes.
The
second approach is subtler. You
create a new class as a type
of
an
existing class. You literally
take the form of the
existing class
and
add code to it, without
modifying the existing
class. This
magical
act is called inheritance, and most
of the work is done by the
compiler.
Inheritance is one of the
cornerstones of object-oriented
programming
and has additional implications
that will be explored
in
Chapter 15.
It
turns out that much of the syntax and
behavior are similar
for
both
composition and inheritance (which makes
sense; they are
both
ways of making new types from existing
types). In this
chapter,
you'll learn about these
code reuse
mechanisms.
Composition
syntax
Actually,
you've been using composition all
along to create
classes.
You've
just been composing classes
primarily with built-in
types
(and
sometimes strings).
It turns out to be almost as easy to
use
composition
with user-defined types.
Consider
a class that is valuable for
some reason:
//:
C14:Useful.h
//
A class to reuse
#ifndef
USEFUL_H
614
Thinking
in C++
#define
USEFUL_H
class
X {
int
i;
public:
X()
{ i = 0; }
void
set(int ii) { i = ii;
}
int
read() const { return i;
}
int
permute() { return i = i * 47;
}
};
#endif
// USEFUL_H ///:~
The
data members are private
in
this class, so it's
completely safe to
embed
an object of type X
as
a public
object
in a new class, which
makes
the interface
straightforward:
//:
C14:Composition.cpp
//
Reuse code with
composition
#include
"Useful.h"
class
Y {
int
i;
public:
X
x; // Embedded object
Y()
{ i = 0; }
void
f(int ii) { i = ii; }
int
g() const { return i;
}
};
int
main() {
Y
y;
y.f(47);
y.x.set(37);
// Access the embedded
object
}
///:~
Accessing
the member functions of the
embedded object
(referred
to
as a subobject) simply
requires another member
selection.
It's
more common to make the
embedded objects private,
so they
become
part of the underlying implementation
(which means you
can
change the implementation if you
want). The public
interface
functions
for your new class then involve the use
of the embedded
object,
but they don't necessarily mimic the
object's interface:
14:
Inheritance & Composition
615
//:
C14:Composition2.cpp
//
Private embedded
objects
#include
"Useful.h"
class
Y {
int
i;
X
x; // Embedded object
public:
Y()
{ i = 0; }
void
f(int ii) { i = ii;
x.set(ii); }
int
g() const { return i *
x.read(); }
void
permute() { x.permute(); }
};
int
main() {
Y
y;
y.f(47);
y.permute();
}
///:~
Here,
the permute(
)function
is carried through to the new
class
interface,
but the other member
functions of X
are
used within the
members
of Y.
Inheritance
syntax
The
syntax for composition is obvious, but to
perform inheritance
there's
a new and different form.
When
you inherit, you are saying,
"This new class is like that
old
class."
You state this in code by
giving the name of the
class as
usual,
but before the opening brace
of the class body, you put
a
colon
and the name of the base
class (or base
classes,
separated by
commas,
for multiple inheritance). When you do this,
you
automatically
get all the data members and
member functions in
the
base class. Here's an
example:
//:
C14:Inheritance.cpp
//
Simple inheritance
#include
"Useful.h"
#include
<iostream>
616
Thinking
in C++
using
namespace std;
class
Y : public X {
int
i; // Different from X's
i
public:
Y()
{ i = 0; }
int
change() {
i
= permute(); // Different name
call
return
i;
}
void
set(int ii) {
i
= ii;
X::set(ii);
// Same-name function
call
}
};
int
main() {
cout
<< "sizeof(X) = " << sizeof(X) <<
endl;
cout
<< "sizeof(Y) = "
<<
sizeof(Y) << endl;
Y
D;
D.change();
//
X function interface comes
through:
D.read();
D.permute();
//
Redefined functions hide
base versions:
D.set(12);
}
///:~
You
can see Y
being
inherited from X,
which means that Y
will
contain
all the data elements in
X
and
all the member functions
in
X.
In fact, Y
contains
a subobject of X
just
as if you had created a
member
object of X
inside
Y
instead
of inheriting from X.
Both
member
objects and base class storage
are referred to as
subobjects.
All
the private
elements
of X
are
still private
in
Y;
that is, just
because
Y
inherits
from X
doesn't
mean Y
can
break the protection
mechanism.
The private
elements
of X
are
still there, they take
up
space
you just can't access them
directly.
In
main(
) you
can see that Y's
data elements are combined
with X's
because
the sizeof(Y)is
twice as big as sizeof(X)
.
14:
Inheritance & Composition
617
You'll
notice that the base class
is preceded by public.
During
inheritance,
everything defaults to private.
If the base class
were
not
preceded by public,
it would mean that all of the
public
members
of the base class would be private
in
the derived class.
all
the public
members
of the base class public
in
the derived class.
You
do this by using the
public
keyword
during inheritance.
In
change(
) the
base-class permute(
)function
is called. The
,
derived
class has direct access to
all the public
base-class
functions.
The
set(
) function
in the derived class redefines
the
set(
) function
in
the
base class. That is, if you
call the functions read(
) and
permute(
)for
an object of type Y,
you'll get the base-class
versions
of
those functions (you can see
this happen inside main(
)).
But if
you
call set(
) for
a Y
object,
you get the redefined
version. This
means
that if you don't like the
version of a function you
get
during
inheritance, you can change what it
does. (You can also
add
completely
new functions like change(
)
.)
However,
when you're redefining a function, you may
still want to
call
the base-class version. If,
inside set(
),
you simply call set(
)
you'll
get the local version of
the function a recursive
function
call.
To call the base-class
version, you must explicitly name
the
base
class using the scope
resolution operator.
The
constructor initializer list
You've
seen how important it is in C++ to
guarantee proper
initialization,
and it's no different during composition
and
inheritance.
When an object is created, the
compiler guarantees
that
constructors
for all of its subobjects are
called. In the examples
so
1
In Java, the compiler
won't let you decrease the
access of a member
during
inheritance.
618
Thinking
in C++
far,
all of the subobjects have
default constructors, and that's
what
the
compiler automatically calls. But what
happens if your
subobjects
don't have default constructors, or if
you want to change
a
default argument in a constructor?
This is a problem because
the
new
class constructor doesn't
have permission to access
the private
data
elements of the subobject, so it
can't initialize them
directly.
The
solution is simple: Call the
constructor for the subobject.
C++
provides
a special syntax for this,
the constructor
initializer list.
The
form
of the constructor initializer
list echoes the act of
inheritance.
With
inheritance, you put the base classes
after a colon and
before
the
opening brace of the class
body. In the constructor
initializer
list,
you put the calls to subobject
constructors after the
constructor
argument
list and a colon, but before
the opening brace of
the
function
body. For a class MyType,
inherited from Bar,
this might
look
like this:
MyType::MyType(int
i) : Bar(i) { // ...
if
Bar
has
a constructor that takes a
single int
argument.
Member
object initialization
It
turns out that you use this very
same syntax for member
object
initialization
when using composition. For
composition, you give
the
names of the objects instead
of the class names. If you
have
more
than one constructor call in
the initializer list, you
separate
the
calls with commas:
MyType2::MyType2(int
i) : Bar(i), m(i+1) { //
...
This
is the beginning of a constructor for
class MyType2,
which is
inherited
from Bar
and
contains a member object
called m.
Note
that
while you can see the type of
the base class in the
constructor
initializer
list, you only see the
member object
identifier.
14:
Inheritance & Composition
619
Built-in
types in the initializer list
The
constructor initializer list
allows you to explicitly call
the
constructors
for member objects. In fact,
there's no other way to
call
those
constructors. The idea is
that the constructors are
all called
before
you get into the body of the new
class's constructor. That
way,
any calls you make to member
functions of subobjects will
always
go to initialized objects. There's no way
to get to the
opening
brace of the constructor without
some
constructor
being
called
for all the member objects and
base-class objects, even if
the
compiler
must make a hidden call to a
default constructor. This is
a
further
enforcement of the C++ guarantee
that no object (or part
of
an
object) can get out of the
starting gate without its
constructor
being
called.
This
idea that all of the member
objects are initialized by
the time
the
opening brace of the
constructor is reached is a
convenient
programming
aid as well. Once you hit the
opening brace, you
can
assume
all subobjects are properly
initialized and focus on
specific
tasks
you want to accomplish in the
constructor. However, there's a
hitch:
What about member objects of
built-in types, which don't
have
constructors?
To
make the syntax consistent,
you are allowed to treat
built-in
types
as if they have a single constructor,
which takes a single
argument:
a variable of the same type as
the variable you're
initializing.
Thus, you can say
//:
C14:PseudoConstructor.cpp
class
X {
int
i;
float
f;
char
c;
char*
s;
public:
X()
: i(7), f(1.4), c('x'),
s("howdy") {}
};
int
main() {
620
Thinking
in C++
X
x;
int
i(100); // Applied to ordinary
definition
int*
ip = new int(47);
}
///:~
The
action of these "pseudo-constructor
calls" is to perform a
simple
assignment. It's a convenient technique
and a good coding
style,
so you'll see it used
often.
It's
even possible to use the
pseudo-constructor syntax when
creating
a variable of a built-in type outside of
a class:
int
i(100);
int*
ip = new int(47);
This
makes built-in types act a
little bit more like
objects.
Remember,
though, that these are not
real constructors. In
particular,
if you don't explicitly make a
pseudo-constructor call,
no
initialization is performed.
Combining
composition & inheritance
Of
course, you can use
composition & inheritance together.
The
following
example shows the creation
of a more complex
class
using
both of them.
//:
C14:Combined.cpp
//
Inheritance & composition
class
A {
int
i;
public:
A(int
ii) : i(ii) {}
~A()
{}
void
f() const {}
};
class
B {
int
i;
public:
B(int
ii) : i(ii) {}
14:
Inheritance & Composition
621
~B()
{}
void
f() const {}
};
class
C : public B {
A
a;
public:
C(int
ii) : B(ii), a(ii) {}
~C()
{} // Calls ~A() and
~B()
void
f() const { //
Redefinition
a.f();
B::f();
}
};
int
main() {
C
c(47);
}
///:~
C
inherits
from B
and
has a member object ("is
composed of") of
type
A.
You can see the constructor
initializer list contains
calls to
both
the base-class constructor and
the member-object
constructor.
The
function C::f(
) redefines
B::f(
),
which it inherits, and also
calls
the
base-class version. In addition, it
calls a.f(
).
Notice that the only
time
you can talk about
redefinition of functions is
during
inheritance;
with a member object you can only
manipulate the
public
interface of the object, not
redefine it. In addition,
calling f(
)
for
an object of class C
would
not call a.f(
) if
C::f(
) had
not been
defined,
whereas it would
call
B::f(
).
Automatic
destructor calls
Although
you are often required to
make explicit constructor
calls
in
the initializer list, you
never need to make explicit
destructor
calls
because there's only one
destructor for any class, and it
doesn't
take any arguments. However, the
compiler still
ensures
that
all destructors are called, and
that means all of the
destructors
in
the entire hierarchy,
starting with the most-derived
destructor
and
working back to the
root.
622
Thinking
in C++
It's
worth emphasizing that constructors and
destructors are quite
unusual
in that every one in the
hierarchy is called, whereas with
a
normal
member function only that
function is called, but not any of
the
base-class versions. If you also want to
call the base-class
version
of a normal member function that
you're overriding, you
must
do it explicitly.
Order
of constructor & destructor calls
It's
interesting to know the order of
constructor and destructor
calls
when
an object has many subobjects.
The following example
shows
exactly
how it works:
//:
C14:Order.cpp
//
Constructor/destructor order
#include
<fstream>
using
namespace std;
ofstream
out("order.out");
#define
CLASS(ID) class ID { \
public:
\
ID(int)
{ out << #ID " constructor\n"; }
\
~ID()
{ out << #ID " destructor\n"; }
\
};
CLASS(Base1);
CLASS(Member1);
CLASS(Member2);
CLASS(Member3);
CLASS(Member4);
class
Derived1 : public Base1
{
Member1
m1;
Member2
m2;
public:
Derived1(int)
: m2(1), m1(2), Base1(3)
{
out
<< "Derived1 constructor\n";
}
~Derived1()
{
out
<< "Derived1 destructor\n";
}
};
14:
Inheritance & Composition
623
class
Derived2 : public Derived1
{
Member3
m3;
Member4
m4;
public:
Derived2()
: m3(1), Derived1(2), m4(3)
{
out
<< "Derived2 constructor\n";
}
~Derived2()
{
out
<< "Derived2 destructor\n";
}
};
int
main() {
Derived2
d2;
}
///:~
First,
an ofstreamobject
is created to send all the output to a
file.
Then,
to save some typing and demonstrate a
macro technique that
will
be replaced by a much improved technique
in Chapter 16, a
macro
is created to build some of the
classes, which are then
used
in
inheritance and composition. Each of
the constructors and
destructors
report themselves to the
trace file. Note that
the
constructors
are not default constructors; they
each have an int
argument.
The argument itself has no
identifier; its only reason
for
existence
is to force you to explicitly call
the constructors in
the
initializer
list. (Eliminating the
identifier prevents
compiler
warning
messages.)
The
output of this program is
Base1
constructor
Member1
constructor
Member2
constructor
Derived1
constructor
Member3
constructor
Member4
constructor
Derived2
constructor
Derived2
destructor
Member4
destructor
Member3
destructor
Derived1
destructor
624
Thinking
in C++
Member2
destructor
Member1
destructor
Base1
destructor
You
can see that construction
starts at the very root of
the class
hierarchy,
and that at each level the
base class constructor is
called
first,
followed by the member
object constructors. The
destructors
are
called in exactly the
reverse order of the
constructors this is
important
because of potential dependencies (in
the derived-class
constructor
or destructor, you must be able to assume
that the base-
class
subobject is still available for
use, and has already
been
constructed
or not destroyed yet).
It's
also interesting that the
order of constructor calls for
member
objects
is completely unaffected by the
order of the calls in
the
constructor
initializer list. The order
is determined by the order
that
the
member objects are declared
in the class. If you could
change
the
order of constructor calls via
the constructor initializer
list, you
could
have two different call
sequences in two different
constructors,
but the poor destructor wouldn't know how
to
properly
reverse the order of the
calls for destruction, and you
could
end up with a dependency
problem.
Name
hiding
If
you inherit a class and provide a new
definition for one of
its
member
functions, there are two
possibilities. The first is
that you
provide
the exact signature and return type in
the derived class
definition
as in the base class definition.
This is called redefining
for
ordinary
member functions and overriding
when the
base class
member
function is a virtual
function
(virtual
functions
are the
normal
case, and will be covered in detail in
Chapter 15). But what
happens
if you change the member
function argument list or
return
type
in the derived class? Here's an
example:
//:
C14:NameHiding.cpp
//
Hiding overloaded names
during inheritance
14:
Inheritance & Composition
625
#include
<iostream>
#include
<string>
using
namespace std;
class
Base {
public:
int
f() const {
cout
<< "Base::f()\n";
return
1;
}
int
f(string) const { return 1;
}
void
g() {}
};
class
Derived1 : public Base
{
public:
void
g() const {}
};
class
Derived2 : public Base
{
public:
//
Redefinition:
int
f() const {
cout
<< "Derived2::f()\n";
return
2;
}
};
class
Derived3 : public Base
{
public:
//
Change return type:
void
f() const { cout <<
"Derived3::f()\n"; }
};
class
Derived4 : public Base
{
public:
//
Change argument list:
int
f(int) const {
cout
<< "Derived4::f()\n";
return
4;
}
};
int
main() {
string
s("hello");
626
Thinking
in C++
Derived1
d1;
int
x = d1.f();
d1.f(s);
Derived2
d2;
x
= d2.f();
//!
d2.f(s); // string version
hidden
Derived3
d3;
//!
x = d3.f(); // return int
version hidden
Derived4
d4;
//!
x = d4.f(); // f() version
hidden
x
= d4.f(1);
}
///:~
In
Base
you
see an overloaded function
f(
),
and Derived1doesn't
make
any changes to f(
) but
it does redefine g(
).
In main(
),
you
can
see that both overloaded
versions of f(
) are
available in
Derived1
However,
Derived2redefines
one overloaded version
of
.
f(
) but
not the other, and the
result is that the second
overloaded
form
is unavailable. In Derived3
changing
the return type hides
,
both
the base class versions, and
Derived4shows
that changing the
argument
list also hides both
the base class versions. In
general, we
can
say that anytime you
redefine an overloaded function
name
from
the base class, all the
other versions are
automatically hidden
in
the new class. In Chapter
15, you'll see that the
addition of the
virtual
keyword
affects function overloading a
bit more.
If
you change the interface of
the base class by modifying
the
signature
and/or return type of a member function from
the base
class,
then you're using the class
in a different way than
inheritance
is
normally intended to support. It doesn't
necessarily mean
you're
doing
it wrong, it's just that the
ultimate goal of inheritance is
to
support
polymorphism, and if you
change the function
signature or
return
type then you are actually changing
the interface of the
base
class.
If this is what you have intended to do
then you are using
inheritance
primarily to reuse code, and not to
maintain the
common
interface of the base class (which is an
essential aspect of
polymorphism).
In general, when you use inheritance
this way it
means
you're taking a general-purpose
class and specializing it for
14:
Inheritance & Composition
627
a
particular need which is usually,
but not always, considered
the
realm
of composition.
For
example, consider the
Stack
class
from Chapter 9. One of the
problems
with that class is that you had to
perform a cast every
time
you fetched a pointer from the
container. This is not only
tedious,
it's unsafe you could
cast the pointer to anything
you
want.
An
approach that seems better
at first glance is to specialize
the
general
Stack
class
using inheritance. Here's an
example that uses
the
class from Chapter 9:
//:
C14:InheritStack.cpp
//
Specializing the Stack
class
#include
"../C09/Stack4.h"
#include
"../require.h"
#include
<iostream>
#include
<fstream>
#include
<string>
using
namespace std;
class
StringStack : public Stack
{
public:
void
push(string* str) {
Stack::push(str);
}
string*
peek() const {
return
(string*)Stack::peek();
}
string*
pop() {
return
(string*)Stack::pop();
}
~StringStack()
{
string*
top = pop();
while(top)
{
delete
top;
top
= pop();
}
}
};
628
Thinking
in C++
int
main() {
ifstream
in("InheritStack.cpp");
assure(in,
"InheritStack.cpp");
string
line;
StringStack
textlines;
while(getline(in,
line))
textlines.push(new
string(line));
string*
s;
while((s
= textlines.pop()) != 0) { // No
cast!
cout
<< *s << endl;
delete
s;
}
}
///:~
Since
all of the member functions in
Stack4.hare
inlines, nothing
needs
to be linked.
StringStackspecializes
Stack
so
that push(
) will
accept only
String
pointers.
Before, Stack
would
accept void
pointers,
so the
user
had no type checking to make sure
the proper pointers
were
inserted.
In addition, peek(
) and
pop(
) now
return String
pointers
instead
of void
pointers,
so no cast is necessary to use
the pointer.
Amazingly
enough, this extra
type-checking safety is free
in
push(
),
peek(
),
and pop(
)!
The compiler is being given
extra type
information
that it uses at compile-time, but
the functions are
inlined
and no extra code is
generated.
Name
hiding comes into play here
because, in particular,
the
push(
) function
has a different signature:
the argument list is
different.
If you had two versions of push(
) in
the same class,
that
would
be overloading, but in this case
overloading is not
what we
want
because that would still allow you to
pass any kind of pointer
into
push(
) as
a void*.
Fortunately, C++ hides the
push(void*)
version
in the base class in favor of
the new version that's
defined
in
the derived class, and
therefore it only allows us to push(
) string
pointers
onto the StringStack
.
14:
Inheritance & Composition
629
Because
we can now guarantee that we know
exactly what kind of
objects
are in the container, the
destructor works correctly and
the
ownership
problem is solved or at least,
one approach to the
ownership
problem. Here, if you push(
) a
string
pointer
onto the
StringStack
then
(according to the semantics of
the StringStack
,
)
you're
also passing ownership of
that pointer to the
StringStack
If
.
you
pop(
) the
pointer, you not only get the
pointer, but you also
get
ownership of that pointer. Any
pointers that are left on
the
StringStackwhen
its destructor is called are
then deleted by that
destructor.
And since these are always
string
pointers
and the
delete
statement
is working on string
pointers
instead of void
pointers,
the proper destruction
happens and everything works
correctly.
There
is a drawback: this class works
only
for
string
pointers.
If you
want
a Stack
that
works with some other kind of object, you
must
write
a new version of the class so
that it works only with your new
kind
of object. This rapidly
becomes tedious, and is finally
solved
using
templates, as you will see in Chapter
16.
We
can make an additional
observation about this
example: it
changes
the interface of the
Stack
in
the process of inheritance.
If
the
interface is different, then a StringStackreally
isn't a Stack,
and
you
will never be able to correctly
use a StringStackas
a Stack.
This
makes the use of inheritance
questionable here; if you're
not
creating
a StringStackthat
is-a
type of
Stack,
then why are you
inheriting?
A more appropriate version of
StringStackwill
be
shown
later in this
chapter.
Functions
that don't automatically
inherit
Not
all functions are automatically
inherited from the base
class
into
the derived class.
Constructors and destructors deal with
the
creation
and destruction of an object, and they
can know what to
630
Thinking
in C++
do
with the aspects of the
object only for their particular
class, so all
the
constructors and destructors in the
hierarchy below them must
be
called. Thus, constructors and
destructors don't inherit and must
be
created specially for each
derived class.
In
addition, the operator=doesn't
inherit because it performs
a
constructor-like
activity. That is, just
because you know how to
assign
all the members of an object on
the left-hand side of the
=
from
an object on the right-hand
side doesn't mean that
assignment
will
still have the same
meaning after
inheritance.
In
lieu of inheritance, these
functions are synthesized by
the
compiler
if you don't create them yourself. (With
constructors, you
can't
create any
constructors
in order for the compiler to
synthesize
the
default constructor and the
copy-constructor.) This was
briefly
described
in Chapter 6. The synthesized
constructors use
memberwise
initialization and the synthesized
operator=uses
memberwise
assignment. Here's an example of
the functions that
are
synthesized by the
compiler:
//:
C14:SynthesizedFunctions.cpp
//
Functions that are
synthesized by the
compiler
#include
<iostream>
using
namespace std;
class
GameBoard {
public:
GameBoard()
{ cout << "GameBoard()\n"; }
GameBoard(const
GameBoard&) {
cout
<< "GameBoard(const
GameBoard&)\n";
}
GameBoard&
operator=(const GameBoard&) {
cout
<< "GameBoard::operator=()\n";
return
*this;
}
~GameBoard()
{ cout << "~GameBoard()\n";
}
};
class
Game {
GameBoard
gb; // Composition
14:
Inheritance & Composition
631
public:
//
Default GameBoard constructor
called:
Game()
{ cout << "Game()\n"; }
//
You must explicitly call
the GameBoard
//
copy-constructor or the default
constructor
//
is automatically called
instead:
Game(const
Game& g) : gb(g.gb) {
cout
<< "Game(const
Game&)\n";
}
Game(int)
{ cout << "Game(int)\n"; }
Game&
operator=(const Game& g) {
//
You must explicitly call
the GameBoard
//
assignment operator or no assignment
at
//
all happens for
gb!
gb
= g.gb;
cout
<< "Game::operator=()\n";
return
*this;
}
class
Other {}; // Nested
class
//
Automatic type
conversion:
operator
Other() const {
cout
<< "Game::operator
Other()\n";
return
Other();
}
~Game()
{ cout << "~Game()\n"; }
};
class
Chess : public Game
{};
void
f(Game::Other) {}
class
Checkers : public Game
{
public:
//
Default base-class constructor
called:
Checkers()
{ cout << "Checkers()\n"; }
//
You must explicitly call
the base-class
//
copy constructor or the
default constructor
//
will be automatically called
instead:
Checkers(const
Checkers& c) : Game(c) {
cout
<< "Checkers(const Checkers&
c)\n";
}
Checkers&
operator=(const Checkers& c) {
//
You must explicitly call
the base-class
//
version of operator=() or no
base-class
//
assignment will
happen:
632
Thinking
in C++
Game::operator=(c);
cout
<< "Checkers::operator=()\n";
return
*this;
}
};
int
main() {
Chess
d1; // Default
constructor
Chess
d2(d1); // Copy-constructor
//!
Chess d3(1); // Error: no
int constructor
d1
= d2; // Operator=
synthesized
f(d1);
// Type-conversion IS inherited
Game::Other
go;
//!
d1 = go; // Operator= not
synthesized
//
for differing types
Checkers
c1, c2(c1);
c1
= c2;
}
///:~
The
constructors and the operator=for
GameBoardand
Game
announce
themselves so you can see when
they're used by the
compiler.
In addition, the operator
Other( )
performs
automatic
type
conversion from a Game
object
to an object of the nested
class
Other.
The class Chess
simply
inherits from Game
and
creates no
functions
(to see how the compiler
responds). The function
f(
)
takes
an Other
object
to test the automatic type
conversion
function.
In
main(
),
the synthesized default
constructor and copy-
constructor
for the derived class
Chess
are
called. The Game
versions
of these constructors are
called as part of the
constructor-
call
hierarchy. Even though it looks
like inheritance, new
constructors
are actually synthesized by
the compiler. As you
might
expect, no constructors with arguments
are automatically
created
because that's too much for
the compiler to
intuit.
The
operator=is
also synthesized as a new function in
Chess
using
memberwise
assignment (thus, the
base-class version is
called)
because
that function was not
explicitly written in the new
class.
14:
Inheritance & Composition
633
And
of course the destructor was
automatically synthesized by
the
compiler.
Because
of all these rules about
rewriting functions that
handle
object
creation, it may seem a little
strange at first that
the
automatic
type conversion operator is
inherited.
But it's not too
unreasonable
if there are enough
pieces in Game
to
make an
Other
object,
those pieces are still
there in anything derived
from
Game
and
the type conversion operator is
still valid (even though
you
may in fact want to redefine
it).
operator=is
synthesized only
for
assigning objects of the
same type.
If
you want to assign one type to another
you must always write
that
operator=yourself.
If
you look more closely at
Game,
you'll see that the
copy-
constructor
and assignment operators have
explicit calls to the
member
object copy-constructor and assignment
operator. You will
normally
want to do this because otherwise, in
the case of the
copy-
constructor,
the default member object
constructor will be used
instead,
and in the case of the
assignment operator, no
assignment
at
all will be done for the member
objects!
Lastly,
look at Checkers
which
explicitly writes out the
default
,
constructor,
copy-constructor, and assignment
operators. In the
case
of the default constructor,
the default base-class
constructor is
automatically
called, and that's typically what you
want. But, and
this
is an important point, as soon as you
decide to write your own
copy-constructor
and assignment operator, the
compiler assumes
that
you know what you're doing and does
not automatically
call
the
base-class versions, as it does in
the synthesized functions.
If
you
want the base class versions
called (and you typically
do) then
you
must explicitly call them yourself. In
the Checkerscopy-
constructor,
this call appears in the
constructor initializer
list:
Checkers(const
Checkers& c) : Game(c) {
634
Thinking
in C++
In
the Checkersassignment
operator, the base class
call is the first
line
in the function body:
Game::operator=(c);
These
calls should be part of the
canonical form that you
use
whenever
you inherit a class.
Inheritance
and static member functions
static
member
functions act the same as
non-static
member
functions:
1.
They
inherit into the derived
class.
2.
If
you redefine a static member, all
the other overloaded
functions
in the base class are
hidden.
3.
If
you change the signature of a
function in the base class,
all
the
base class versions with that
function name are
hidden
(this
is really a variation of the
previous point).
However,
static
member
functions cannot be virtual
(a
topic
covered
thoroughly in Chapter 15).
Choosing
composition vs. inheritance
Both
composition and inheritance place
subobjects inside your new
class.
Both use the constructor
initializer list to construct
these
subobjects.
You may now be wondering what the
difference is
between
the two, and when to choose one
over the other.
Composition
is generally used when you want the
features of an
existing
class inside your new class, but not
its interface. That
is,
you
embed an object to implement
features of your new class, but
the
user of your new class sees
the interface you've defined
rather
than
the interface from the
original class. To do this, you follow
the
14:
Inheritance & Composition
635
typical
path of embedding private
objects
of existing classes inside
your
new class.
Occasionally,
however, it makes sense to allow
the class user to
directly
access the composition of your new
class, that is, to
make
the
member objects public.
The member objects use
access control
themselves,
so this is a safe thing to do and when
the user knows
you're
assembling a bunch of parts, it makes
the interface easier
to
understand.
A Car
class
is a good example:
//:
C14:Car.cpp
//
Public composition
class
Engine {
public:
void
start() const {}
void
rev() const {}
void
stop() const {}
};
class
Wheel {
public:
void
inflate(int psi) const
{}
};
class
Window {
public:
void
rollup() const {}
void
rolldown() const {}
};
class
Door {
public:
Window
window;
void
open() const {}
void
close() const {}
};
class
Car {
public:
Engine
engine;
Wheel
wheel[4];
Door
left, right; //
2-door
636
Thinking
in C++
};
int
main() {
Car
car;
car.left.window.rollup();
car.wheel[0].inflate(72);
}
///:~
Because
the composition of a Car
is
part of the analysis of
the
problem
(and not simply part of the underlying
design), making
the
members public
assists
the client programmer's
understanding
of
how to use the class and
requires less code
complexity for the
creator
of the class.
With
a little thought, you'll also
see that it would make no
sense to
compose
a Car
using
a "vehicle" object a car
doesn't contain a
vehicle,
it is
a vehicle.
The is-a
relationship
is expressed with
inheritance,
and the has-a
relationship
is expressed with
composition.
Subtyping
Now
suppose you want to create a type of
ifstreamobject
that not
only
opens a file but also keeps
track of the name of the
file. You
can
use composition and embed
both an ifstreamand
a string
into
the
new class:
//:
C14:FName1.cpp
//
An fstream with a file
name
#include
"../require.h"
#include
<iostream>
#include
<fstream>
#include
<string>
using
namespace std;
class
FName1 {
ifstream
file;
string
fileName;
bool
named;
public:
FName1()
: named(false) {}
14:
Inheritance & Composition
637
FName1(const
string& fname)
:
fileName(fname), file(fname.c_str())
{
assure(file,
fileName);
named
= true;
}
string
name() const { return
fileName; }
void
name(const string& newName) {
if(named)
return; // Don't
overwrite
fileName
= newName;
named
= true;
}
operator
ifstream&() { return file;
}
};
int
main() {
FName1
file("FName1.cpp");
cout
<< file.name() <<
endl;
//
Error: close() not a
member:
//!
file.close();
}
///:~
There's
a problem here, however. An
attempt is made to allow
the
use
of the FName1
object
anywhere an ifstreamobject
is used by
including
an automatic type conversion operator
from FName1
to
an
ifstream&
But
in main, the line
.
file.close();
will
not compile because automatic
type conversion happens
only
in
function calls, not during member
selection. So this
approach
won't
work.
A
second approach is to add
the definition of close(
)to
FName1:
void
close() { file.close(); }
This
will work if there are only a few
functions you want to bring
through
from the ifstreamclass.
In that case you're only
using part
of
the class, and composition is
appropriate.
But
what if you want everything in the class
to come through? This
is
called subtyping
because
you're making a new type from an
638
Thinking
in C++
existing
type, and you want your new type to have
exactly the
same
interface as the existing type
(plus any other
member
functions
you want to add), so you can use it
everywhere you'd use
the
existing type. This is where
inheritance is essential. You can
see
that
subtyping solves the problem
in the preceding
example
perfectly:
//:
C14:FName2.cpp
//
Subtyping solves the
problem
#include
"../require.h"
#include
<iostream>
#include
<fstream>
#include
<string>
using
namespace std;
class
FName2 : public ifstream
{
string
fileName;
bool
named;
public:
FName2()
: named(false) {}
FName2(const
string& fname)
:
ifstream(fname.c_str()), fileName(fname)
{
assure(*this,
fileName);
named
= true;
}
string
name() const { return
fileName; }
void
name(const string& newName) {
if(named)
return; // Don't
overwrite
fileName
= newName;
named
= true;
}
};
int
main() {
FName2
file("FName2.cpp");
assure(file,
"FName2.cpp");
cout
<< "name: " << file.name() <<
endl;
string
s;
getline(file,
s); // These work
too!
file.seekg(-200,
ios::end);
file.close();
}
///:~
14:
Inheritance & Composition
639
Now
any member function available for an
ifstreamobject
is
available
for an FName2
object.
You can also see that
non-member
functions
like getline(
)that
expect an ifstreamcan
also work with
an
FName2.
That's because an FName2
is a type of
ifstream
it
;
doesn't
simply contain one. This is a very
important issue that
will
be
explored at the end of this
chapter and in the next
one.
private
inheritance
You
can inherit a base class
privately by leaving off the
public
in
the
base-class list, or by explicitly
saying private
(probably
a better
policy
because it is clear to the
user that you mean it). When
you
inherit
privately, you're "implementing in
terms of;" that is,
you're
creating
a new class that has all of
the data and functionality of
the
base
class, but that functionality is
hidden, so it's only part of
the
underlying
implementation. The class
user has no access to
the
underlying
functionality, and an object cannot be
treated as a
instance
of the base class (as it was
in FName2.cpp
).
You
may wonder what the purpose of private
inheritance
is,
because
the alternative of using
composition to create a private
object
in the new class seems more
appropriate. private
inheritance
is
included in the language for
completeness, but if for no other
reason
than to reduce confusion, you'll usually want to
use
composition
rather than private
inheritance.
However, there may
occasionally
be situations where you want to produce
part of the
same
interface as the base class
and
disallow
the treatment of the
object
as if it were a base-class object.
private
inheritance
provides
this
ability.
Publicizing
privately inherited members
When
you inherit privately, all the
public
members
of the base
class
become private.
If you want any of them to be visible, just
say
their
names (no arguments or return values) in
the public
section
of
the
derived class:
//:
C14:PrivateInheritance.cpp
640
Thinking
in C++
class
Pet {
public:
char
eat() const { return 'a';
}
int
speak() const { return 2;
}
float
sleep() const { return 3.0;
}
float
sleep(int) const { return
4.0; }
};
class
Goldfish : Pet { // Private
inheritance
public:
Pet::eat;
// Name publicizes
member
Pet::sleep;
// Both overloaded members
exposed
};
int
main() {
Goldfish
bob;
bob.eat();
bob.sleep();
bob.sleep(1);
//!
bob.speak();// Error: private
member function
}
///:~
Thus,
private
inheritance
is useful if you want to hide part of
the
functionality
of the base class.
Notice
that giving the name of an
overloaded function exposes
all
the
versions of the overloaded
function in the base
class.
You
should think carefully before
using private
inheritance
instead
of
composition; private
inheritance
has particular
complications
when
combined with runtime type identification
(this is the topic of
a
chapter in Volume 2 of this book,
downloadable from
).
protected
Now
that you've been introduced to
inheritance, the keyword
protectedfinally
has meaning. In an ideal world,
private
members
would
always be hard-and-fast private,
but in real projects
there
are
times when you want to make something
hidden from the
14:
Inheritance & Composition
641
world
at large and yet allow access for members
of derived classes.
The
protectedkeyword
is a nod to pragmatism; it says, "This
is
private
as
far as the class user is
concerned, but available to
anyone
who
inherits from this
class."
The
best approach is to leave
the data members private
you
should
always preserve your right to change
the underlying
implementation.
You can then allow controlled access to
inheritors
of
your class through protectedmember
functions:
//:
C14:Protected.cpp
//
The protected keyword
#include
<fstream>
using
namespace std;
class
Base {
int
i;
protected:
int
read() const
{
return i; }
void
set(int ii)
{
i = ii; }
public:
Base(int
ii = 0)
:
i(ii) {}
int
value(int m)
const
{ return m*i; }
};
class
Derived : public Base
{
int
j;
public:
Derived(int
jj = 0) : j(jj) {}
void
change(int x) { set(x); }
};
int
main() {
Derived
d;
d.change(10);
}
///:~
You
will find examples of the need for
protectedin
examples later
in
this book, and in Volume 2.
642
Thinking
in C++
protected
inheritance
When
you're inheriting, the base
class defaults to private,
which
means
that all of the public
member functions are
private
to
the
user
of the new class. Normally, you'll make
the inheritance public
so
the interface of the base
class is also the interface
of the derived
class.
However, you can also use
the protectedkeyword
during
inheritance.
Protected
derivation means
"implemented-in-terms-of" to other
classes
but "is-a" for derived classes and
friends. It's something
you
don't use very often, but it's in
the language for
completeness.
Operator
overloading & inheritance
Except
for the assignment operator,
operators are
automatically
inherited
into a derived class. This
can be demonstrated by
inheriting
from C12:Byte.h
:
//:
C14:OperatorInheritance.cpp
//
Inheriting overloaded
operators
#include
"../C12/Byte.h"
#include
<fstream>
using
namespace std;
ofstream
out("ByteTest.out");
class
Byte2 : public Byte {
public:
//
Constructors don't
inherit:
Byte2(unsigned
char bb = 0) : Byte(bb) {}
//
operator= does not inherit,
but
//
is synthesized for memberwise
assignment.
//
However, only the SameType =
SameType
//
operator= is synthesized, so you
have to
//
make the others
explicitly:
Byte2&
operator=(const Byte& right) {
Byte::operator=(right);
return
*this;
}
Byte2&
operator=(int i) {
Byte::operator=(i);
14:
Inheritance & Composition
643
return
*this;
}
};
//
Similar test function as in
C12:ByteTest.cpp:
void
k(Byte2& b1, Byte2& b2) {
b1
= b1 * b2 + b2 % b1;
#define
TRY2(OP) \
out
<< "b1 = "; b1.print(out); \
out
<< ", b2 = "; b2.print(out); \
out
<< "; b1 " #OP " b2 produces ";
\
(b1
OP b2).print(out); \
out
<< endl;
b1
= 9; b2 = 47;
TRY2(+)
TRY2(-) TRY2(*)
TRY2(/)
TRY2(%)
TRY2(^) TRY2(&) TRY2(|)
TRY2(<<)
TRY2(>>) TRY2(+=)
TRY2(-=)
TRY2(*=)
TRY2(/=) TRY2(%=)
TRY2(^=)
TRY2(&=)
TRY2(|=) TRY2(>>=)
TRY2(<<=)
TRY2(=)
// Assignment operator
//
Conditionals:
#define
TRYC2(OP) \
out
<< "b1 = "; b1.print(out); \
out
<< ", b2 = "; b2.print(out); \
out
<< "; b1 " #OP " b2 produces ";
\
out
<< (b1 OP b2); \
out
<< endl;
b1
= 9; b2 = 47;
TRYC2(<)
TRYC2(>) TRYC2(==) TRYC2(!=)
TRYC2(<=)
TRYC2(>=)
TRYC2(&&) TRYC2(||)
//
Chained assignment:
Byte2
b3 = 92;
b1
= b2 = b3;
}
int
main() {
out
<< "member functions:" <<
endl;
Byte2
b1(47), b2(9);
k(b1,
b2);
}
///:~
644
Thinking
in C++
The
test code is identical to
that in C12:ByteTest.cpp
except
that
Byte2
is
used instead of Byte.
This way all the operators
are
verified
to work with Byte2
via
inheritance.
When
you examine the class
Byte2,
you'll see that the
constructor
must
be explicitly defined, and that only
the operator=that
assigns
a
Byte2
to
a Byte2
is
synthesized; any other assignment
operators
that
you need you'll have to synthesize on
your own.
Multiple
inheritance
You
can inherit from one class,
so it would seem to make sense
to
inherit
from more than one class at a
time. Indeed you can,
but
whether
it makes sense as part of a
design is a subject of
continuing
debate.
One thing is generally agreed upon: You
shouldn't try this
until
you've been programming quite a while and
understand the
language
thoroughly. By that time, you'll probably
realize that no
matter
how much you think you absolutely must use
multiple
inheritance,
you can almost always get
away with single
inheritance.
Initially,
multiple inheritance seems simple
enough: You add more
classes
in the base-class list during
inheritance, separated by
commas.
However, multiple inheritance introduces a number
of
possibilities
for ambiguity, which is why a chapter in Volume 2
is
devoted
to the subject.
Incremental
development
One
of the advantages of inheritance and
composition is that
these
support
incremental
development by
allowing you to introduce new
code
without causing bugs in existing
code. If bugs do appear,
they
are
isolated within the new code. By
inheriting from (or
composing
with)
an existing, functional class and
adding data members
and
member
functions (and redefining
existing member
functions
14:
Inheritance & Composition
645
during
inheritance) you leave the
existing code that
someone else
may
still be using untouched and
unbugged. If a bug happens,
you
know it's in your new code, which is much
shorter and easier
to
read than if you had modified the body of
existing code.
It's
rather amazing how cleanly
the classes are separated.
You don't
even
need the source code for
the member functions in
order to
reuse
the code, just the
header file describing the
class and the
object
file or library file with
the compiled member
functions. (This
is
true for both inheritance and
composition.)
It's
important to realize that
program development is an
incremental
process, just like human
learning. You can do as much
analysis
as you want, but you still won't know all the
answers
when
you set out on a project. You'll have
much more success
and
more immediate feedback if you
start out to "grow" your
project
as an organic, evolutionary creature,
rather than
Although
inheritance for experimentation is a
useful technique, at
some
point after things stabilize
you need to take a new look
at
your
class hierarchy with an eye to
collapsing it into a sensible
express
a relationship that says,
"This new class is a type
of that
old
class."
Your program should not be concerned with
pushing bits
around,
but instead with creating and
manipulating objects of
various
types to express a model in
the terms given you from
the
problem
space.
2
To learn more about
this idea, see
Extreme
Programming Explained, by Kent
Beck
(Addison-Wesley
2000).
3
See Refactoring:
Improving the Design of Existing
Code by Martin
Fowler (Addison-
Wesley
1999).
646
Thinking
in C++
Upcasting
Earlier
in the chapter, you saw how an
object of a class
derived
from
ifstreamhas
all the characteristics and behaviors of
an
ifstreamobject.
In FName2.cpp
any
ifstreammember
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 Instrumentthat
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 Instrumentclass
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:Instrument.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);
14:
Inheritance & Composition
647
}
int
main() {
Wind
flute;
tune(flute);
// Upcasting
}
///:~
What's
interesting in this example is
the tune(
) function,
which
accepts
an Instrumentreference.
However, in main(
) the
tune(
)
function
is called by handing it a reference to 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
Instrumentthat
isn't also in Wind
(this
is what inheritance
guarantees).
Inside tune(
),
the code works for Instrumentand
anything
derived from Instrument
and
the act of converting
a
,
Wind
reference
or pointer into an Instrumentreference
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 Instrument.cppis
then:
Instrument
Wind
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
648
Thinking
in C++
allows
upcasting without any explicit casts or
other special
notation.
Upcasting
and the copy-constructor
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:CopyConstructor.cpp
//
Correctly creating the
copy-constructor
#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(const
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(const
Member&)\n";
}
friend
ostream&
operator<<(ostream&
os, const Member& m) {
14:
Inheritance & Composition
649
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&
c){
return
os << (Parent&)c <<
c.m
<<
"Child: " << c.i <<
endl;
}
};
int
main() {
Child
c(2);
cout
<< "calling copy-constructor: " <<
endl;
Child
c2 = c; // Calls copy-constructor
cout
<< "values in c2:\n" <<
c2;
}
///:~
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 base-class object
instead
of
a reference you will usually get
undesirable results):
return
os << (Parent&)c <<
c.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
650
Thinking
in C++
Parent(int
ii)
Member(int
ii)
Child(int
ii)
calling
copy-constructor:
Parent(const
Parent&)
Member(const
Member&)
values
in c2:
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& c) : i(c.i), m(c.m) {}
then
the default
constructor
will automatically be called for
the
base-class
part of Child,
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(const
Member&)
values
in c2:
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:
14:
Inheritance & Composition
651
Child(const
Child& c)
:
Parent(c), i(c.i), m(c.m)
{
cout
<< "Child(Child&)\n";
}
The
strange part is where the
Parent
copy-constructor
is called:
Parent(c)
What
does it mean to pass a
Child
object
to a Parent
.
constructor?
But Child
is
inherited from Parent,
so a Child
reference
is
a
Parent
reference.
The base-class copy-constructor
call
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 almost always want to do the
same thing.
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
StringStackobjects
will be used only as string
containers
and
never
upcast, so a more appropriate
alternative is composition:
//:
C14:InheritStack2.cpp
//
Composition vs.
inheritance
#include
"../C09/Stack4.h"
#include
"../require.h"
#include
<iostream>
#include
<fstream>
#include
<string>
using
namespace std;
class
StringStack {
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();
652
Thinking
in C++
}
};
int
main() {
ifstream
in("InheritStack2.cpp");
assure(in,
"InheritStack2.cpp");
string
line;
StringStack
textlines;
while(getline(in,
line))
textlines.push(new
string(line));
string*
s;
while((s
= textlines.pop()) != 0) // No
cast!
cout
<< *s << endl;
}
///:~
The
file is identical to InheritStack.cppexcept
that a Stack
object
is
,
embedded
in StringStack
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.
Although
it tends to be more confusing, you
could also use private
inheritance
to express "implemented in terms of."
This would also
solve
the problem adequately. One
place it becomes
important,
however,
is when multiple inheritance might be warranted. In
that
case,
if you see a design in which composition
can be used instead
of
inheritance, you may be able to eliminate
the need for multiple
inheritance.
Pointer
& reference upcasting
In
Instrument.cpp
the
upcasting occurs during the
function call a
,
Wind
object
outside the function has
its reference taken
and
becomes
an Instrumentreference
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
14:
Inheritance & Composition
653
Like
the function call, neither
of these cases requires 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 Instrumentpointer
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
ip->play(middleC);
the
compiler can know only that
it's calling play(
) for
an
Instrumentpointer,
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
Chapter 15 by
introducing
the third cornerstone of object-oriented
programming:
polymorphism
(implemented in C++ with virtual
functions).
Summary
Both
inheritance and composition allow you to
create a new type
from
existing types, and both
embed subobjects of the
existing
types
inside the new type.
Typically, however, you
use
composition
to reuse existing types as
part of the underlying
implementation
of the new type and inheritance when you want
to
force
the new type to be the same type as
the base class (type
equivalence
guarantees interface equivalence).
Since the derived
class
has the base-class
interface, it can be upcast
to the
base, which
is
critical for polymorphism as you'll see
in Chapter 15.
654
Thinking
in C++
Although
code reuse through composition and
inheritance is very
helpful
for rapid project development, you'll
generally want to
redesign
your class hierarchy before
allowing other
programmers
to
become dependent on it. Your
goal is a hierarchy in which
each
class
has a specific use and is
neither too big
(encompassing so
much
functionality that it's unwieldy to
reuse) nor annoyingly
small
(you can't use it by itself or without
adding functionality).
Exercises
Solutions
to selected exercises can be found in
the electronic document
The
Thinking in C++
Annotated
Solution
Guide,
available for a small fee
from .
1.
Modify
Car.cpp
so
that it also inherits from a
class called
Vehicle,
placing appropriate member
functions in
Vehicle
(that
is, make up some member
functions). Add
a
nondefault constructor to Vehicle,
which you must call
inside
Car's
constructor.
2.
Create
two classes, A
and
B,
with
default constructors
that
announce themselves. Inherit a new class
called C
from
A,
and create a member object of
B
in
C,
but do not
create
a constructor for C.
Create an object of class
C
and
observe
the results.
3.
Create
a three-level hierarchy of classes with
default
constructors,
along with destructors, both of
which
announce
themselves to cout.
Verify that for an object of
the
most derived type, all three
constructors and
destructors
are automatically called.
Explain the order in
which
the calls are
made.
4.
Modify
Combined.cppto
add another level of
inheritance
and a new member object. Add code to
show
when
the constructors and destructors
are being called.
5.
In
Combined.cpp
create
a class D
that
inherits from B
,
and
has a member object of class
C.
Add code to show
when
the constructors and destructors
are being called.
14:
Inheritance & Composition
655
6.
Modify
Order.cppto
add another level of
inheritance
Derived3with
member objects of class
Member4
and
Member5.
Trace the output of the
program.
7.
In
NameHiding.cpp
verify
that in Derived2
Derived3
,
,
,
and
Derived4
none
of the base-class versions of
f(
) are
,
available.
8.
Modify
NameHiding.cppby
adding three
overloaded
functions
named h(
) to
Base,
and show that redefining
one
of them in a derived class hides
the others.
9.
Inherit
a class StringVectorfrom
vector<void*>and
redefine
the push_back(
)and
operator[]member
functions
to accept and produce string*.
What happens if
you
try to push_back(
)a
void*?
10.
Write
a class containing a long
and
use the psuedo-
constructor
call syntax in the
constructor to initialize
the
long.
11.
Create
a class called Asteroid
Use
inheritance to
.
specialize
the PStash
class
in Chapter 13 (PStash.h&
PStash.cpp
so
that it accepts and returns
Asteroid
)
pointers.
Also modify PStashTest.cppto
test your
classes.
Change the class so
PStash
is
a member object.
12.
Repeat
Exercise 11 with a vector
instead
of a PStash.
13.
In
SynthesizedFunctions.cppmodify
Chess
to
give it a
,
default
constructor, copy-constructor, and
assignment
operator.
Demonstrate that you've written
these
correctly.
14.
Create
two classes called Travelerand
Pager
without
default
constructors, but with constructors that
take an
argument
of type string,
which they simply copy to an
internal
string
variable.
For each class, write the
correct
copy-constructor
and assignment operator. Now inherit
a
class
BusinessTravelerfrom
Travelerand
give it a
member
object of type Pager.
Write the correct
default
constructor,
a constructor that takes a string
argument,
a
copy-constructor,
and an assignment operator.
656
Thinking
in C++
15.
Create
a class with two static
member
functions. Inherit
from
this class and redefine one
of the member
functions.
Show
that the other is hidden in
the derived class.
16.
Look
up more of the member
functions for ifstream
In
.
FName2.cpp
try
them out on the file
object.
,
17.
Use
private
and
protectedinheritance
to create two new
classes
from a base class. Then attempt to upcast
objects
of
the derived class to the
base class. Explain what
happens.
18.
In
Protected.cpp
add
a member function in Derived
that
,
calls
the protectedBase
member
read(
).
19.
Change
Protected.cppso
that Derived
is
using protected
inheritance.
See if you can call
value(
)for
a Derived
object.
20.
Create
a class called SpaceShipwith
a fly(
) method.
Inherit
Shuttle
from
SpaceShipand
add a land(
)
method.
Create a new Shuttle,
upcast by pointer or
reference
to a SpaceShip
and
try to call the land(
)
,
method.
Explain the results.
21.
Modify
Instrument.cppto
add a prepare(
)method
to
Instrument
Call
prepare(
)inside
tune(
).
.
22.
Modify
Instrument.cppso
that play(
) prints
a message
to
cout,
and Wind
redefines
play(
) to
print a different
message
to cout.
Run the program and explain why
you
probably
wouldn't want this behavior. Now put
the
virtual
keyword
(which you will learn about in
Chapter
15)
in front of the play(
) declaration
in Instrumentand
observe
the change in the
behavior.
23.
In
CopyConstructor.cppinherit
a new class from Child
,
and
give it a Member
m.
Write a proper constructor
,
copy-constructoroperator=
and
operator<<for
,
,
ostreams,
and test the class in
main(
).
24.
Take
the example CopyConstructor.cpp
and
modify it by
adding
your own copy-constructor to Child
without
calling
the base-class copy-constructor and
see what
14:
Inheritance & Composition
657
happens.
Fix the problem by making a
proper explicit
call
to the base-class copy
constructor in the
constructor-
initializer
list of the Child
copy-constructor.
25.
Modify
InheritStack2.cpp
use
a vector<string>
to
instead
of a Stack.
26.
Create
a class Rock
with
a default constructor, a
copy-
constructor,
an assignment operator, and a destructor,
all
of
which announce to cout
that
they've been called.
In
main(
),
create a vector<Rock>(that
is, hold Rock
objects
by
value) and add some
Rocks.
Run the program and
explain
the output you get. Note whether
the destructors
are
called for the Rock
objects
in the vector.
Now repeat
the
exercise with a vector<Rock*>
Is
it possible to create
.
a
vector<Rock&>
?
27.
This
exercise creates the design
pattern called proxy.
Start
with
a base class Subject
and
give it three functions:
f(
),
g(
),
and h(
).
Now inherit a class Proxy
and
two classes
Implementation1and
Implementation2from
Subject.
Proxy
should
contain a pointer to a Subject,
and all the
member
functions for Proxy
should
just turn around and
make
the same calls through the
Subject
pointer.
The
Proxy
constructor
takes a pointer to a Subject
that
is
installed
in the Proxy
(usually
by the constructor). In
main(
),
create two different Proxy
objects
that use the
two
different implementations. Now modify
Proxy
so
that
you can dynamically change
implementations.
28.
Modify
ArrayOperatorNew.cpp
from
Chapter 13 to
show
that, if you inherit from Widget,
the allocation still
works
correctly. Explain why inheritance in
Framis.cpp
from
Chapter 13 would not
work
correctly.
29.
Modify
Framis.cppfrom
Chapter 13 by inheriting from
Framis
and
creating new versions of new
and
delete
for
your
derived class. Demonstrate
that they work correctly.
658
Thinking
in C++
Table of Contents:
|
|||||