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.
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.
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
#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:
//: 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.
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>
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).
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. This is
almost never what you
want[51];
the desired result is to keep 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.
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 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) { // ...
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.
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() {
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.
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) {}
~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( ).
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.
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.
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";
}
};
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
ofstream object 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 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.
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
#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");
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 Derived1 doesn’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
Derived2 redefines 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 Derived4 shows 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 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();
}
}
};
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.h are inlines
nothing needs to be linked.
StringStack specializes
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.
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 StringStack
when 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 StringStack really
isn’t a Stack
and you will never be able to correctly use a
StringStack as a Stack. This makes the use of inheritance
questionable here; if you’re not creating a StringStack that
is-a type of Stack
then why are you
inheriting? A more appropriate version of StringStack will be shown later
in this
chapter.
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 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
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:
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 GameBoard and 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. 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 Checkers copy-constructor
this call appears in the
constructor initializer list:
Checkers(const Checkers& c) : Game(c) {
In the Checkers assignment
operator
the base class call is the first line in the function
body:
Game::operator=(c);
static member functions act the
same as non-static member functions:
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 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
};
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.
Now suppose you want to create a type of
ifstream object that not only opens a file but
also keeps track of the name of the file. You can use composition and embed both
an ifstream and 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) {}
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
ifstream object 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 ifstream class. 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 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();
} ///:~
Now any member function available for an
ifstream object is available for an FName2 object. You can also
see that non-member functions like getline( ) that expect an
ifstream can 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.
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.
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)
along with the using keyword in the public section of the
derived class:
//: C14:PrivateInheritance.cpp
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:
using Pet::eat; // Name publicizes member
using 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
exposing 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
www.BruceEckel.com).
Now that you’ve been introduced to
inheritance
the keyword
protected finally 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 world at large and yet allow
access for members of derived classes. The protected keyword 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 protected member
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);
} ///:~
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
protected keyword 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.
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);
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);
} ///:~
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.
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.
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 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 constructing it all at once like a glass-box
skyscraper[52].
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
structure[53].
Remember that underneath it all
inheritance is meant to 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.
Earlier in the chapter
you saw how an
object of a class derived from ifstream has all the characteristics and
behaviors of an ifstream object. In FName2.cpp
any ifstream
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: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);
}
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 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 Instrument that isn’t also in
Wind (this is what inheritance guarantees). Inside tune( )
the code works for Instrument and anything derived from
Instrument
and the act of converting a Wind reference or pointer
into an Instrument reference or pointer is called
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.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.
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) {
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
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:
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.
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 StringStack objects 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();
}
};
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.cpp
except 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.
In Instrument.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
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
ip->play(middleC);
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 Chapter 15 by introducing the third cornerstone of object-oriented
programming: polymorphism (implemented in C++ with virtual
functions).
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.
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).
Solutions to selected exercises
can be found in the electronic document The Thinking in C++ Annotated
Solution Guide
available for a small fee from www.BruceEckel.com.
[51]
In Java
the compiler won’t let you decrease the access of a member during
inheritance.
[52]
To learn more about this idea
see Extreme Programming Explained
by Kent
Beck (Addison-Wesley 2000).
[53]
See Refactoring: Improving the Design of Existing Code by Martin Fowler
(Addison-Wesley 1999).