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) 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: 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
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).