Choosing
composition vs. inheritance
Both
composition and inheritance place subobjects
inside your new class. Both use the constructor initializer list to construct
these subobjects. You may now be wondering what the difference is between the
two, and when to choose one over the other.
Composition
is generally used when you want the features of an existing class inside your
new class, but not its interface. That is, you embed an object that
you’re planning on using 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. For this effect, you embed
private
objects of existing classes inside your new class.
Sometimes
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 implementation hiding 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
object 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.
Subtyping
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
strstream
into
the new class:
//: C14:FName1.cpp
// An fstream with a file name
#include <iostream>
#include <fstream>
#include <strstream>
#include "../require.h"
using namespace std;
class FName1 {
ifstream File;
enum { bsize = 100 };
char buf[bsize];
ostrstream Name;
int nameset;
public:
FName1() : Name(buf, bsize), nameset(0) {}
FName1(const char* filename)
: File(filename), Name(buf, bsize) {
assure(File, filename);
Name << filename << ends;
nameset = 1;
}
const char* name() const { return buf; }
void name(const char* newname) {
if(nameset) return; // Don't overwrite
Name << newname << ends;
nameset = 1;
}
operator ifstream&() { return File; }
};
int main() {
FName1 file("FName1.cpp");
cout << file.name() << endl;
// Error: rdbuf() not a member:
//! cout << file.rdbuf() << endl;
} ///:~
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
cout
<< file.rdbuf() << endl;
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
rdbuf( )
to
FName1: filebuf*
rdbuf() { return File.rdbuf(); }
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 <iostream>
#include <fstream>
#include <strstream>
#include "../require.h"
using namespace std;
class FName2 : public ifstream {
enum { bsize = 100 };
char buf[bsize];
ostrstream fname;
int nameset;
public:
FName2() : fname(buf, bsize), nameset(0) {}
FName2(const char* filename)
: ifstream(filename), fname(buf, bsize) {
assure(*this, filename);
fname << filename << ends;
nameset = 1;
}
const char* name() const { return buf; }
void name(const char* newname) {
if(nameset) return; // Don't overwrite
fname << newname << ends;
nameset = 1;
}
};
int main() {
FName2 file("FName2.cpp");
assure(file, "FName2.cpp");
cout << "name: " << file.name() << endl;
const int bsize = 100;
char buf[bsize];
file.getline(buf, bsize); // This works too!
file.seekg(-200, ios::end);
cout << file.rdbuf() << endl;
} ///:~
Now
any member function that works with an
ifstream
object also works with an
FName2
object. 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.
Specialization
When
you inherit, you take an existing class and make a special version of it.
Generally, this means you’re taking a general-purpose class and
specializing it for a particular need.
For
example, consider the
Stack
class from the previous chapter. 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 the
previous chapter:
//: C14:Inhstak.cpp
//{L} ../C13/Stack11
// Specializing the Stack class
#include <iostream>
#include <fstream>
#include <string>
#include "../require.h"
#include "../C13/Stack11.h"
using namespace std;
class StringList : public Stack {
public:
void push(string* str) {
Stack::push(str);
}
string* peek() const {
return (string*)Stack::peek();
}
string* pop() {
return (string*)Stack::pop();
}
};
int main() {
ifstream file("Inhstak.cpp");
assure(file, "Inhstak.cpp");
string line;
StringList textlines;
while(getline(file,line))
textlines.push(new string(line));
string* s;
while((s = textlines.pop()) != 0) // No cast!
cout << *s << endl;
} ///:~
The
Stack11.h
header file is brought in from Chapter 13. (The Stack11 object file must be
linked in as well.)
Stringlist
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 rather than
void
pointers, so no cast is necessary to use the pointer.
Amazingly
enough, this extra type-checking safety is free! The compiler is being given
extra type information, that it uses at compile-time, but the functions are
inline and no extra code is generated.
Unfortunately,
inheritance doesn’t solve all the problems with this container class. The
destructor still causes trouble. You’ll remember from Chapter 11 that the
Stack::~Stack( )
destructor moves through the list and calls
delete
for all the pointers. The problem is,
delete
is called for
void
pointers, which only releases the memory and doesn’t call the destructors
(because
void*
has no type information). If a
Stringlist::~Stringlist( )
destructor is created to move through the list and call
delete
for all the
String
pointers in the list, the problem is solved
if
- The
Stack
data members are made
protected
so the
Stringlist
destructor can access them. (
protected
is described a bit later in the chapter.)
- The
Stack
base class destructor is removed so the memory isn’t released twice.
- No
more inheritance is performed, because you’d end up with the same dilemma
again: multiple destructor calls versus an incorrect destructor call (to a
String
object rather than what the class derived from
Stringlist
might contain).
This
issue will be revisited in the next chapter, but will not be fully solved until
templates are introduced in Chapter XX.
A
more important observation to make about this example is that it
changes
the interface
of the
Stack
in the process of inheritance. If the interface is different, then a
Stringlist
really isn’t a
Stack,
and you will never be able to correctly use a
Stringlist
as a
Stack.
This questions the use of inheritance here: if you’re not creating a
Stringlist
that
is-a
type of
Stack,
then why are you inheriting? A more appropriate version of
Stringlist
will be shown later in the chapter.
private
inheritance
You
can inherit a base class privately by leaving off the
public
in the base-class list, or by explicitly saying
private
(probably a better policy because it is clear to the user that you mean it).
When you inherit privately, you’re “implementing in terms
of”; that is, you’re creating a new class that has all 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
member of the base class (as it was in
FName2.cpp). You
may wonder what the purpose of
private
inheritance is, because the alternative of creating 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 a
private
member rather than
private
inheritance. However, there may occasionally be situations where you want to
produce part of the same interface as the base class
and
disallow the treatment of the object as if it were a base-class object.
private
inheritance provides this ability.
Publicizing
privately inherited members
When
you inherit privately, all the
public
members of the base class become
private.
If you want any of them to be visible, just say their names (no arguments or
return values) in the
public
section of the derived class:
//: C14:Privinh.cpp
// Private inheritance
class Base1 {
public:
char f() const { return 'a'; }
int g() const { return 2; }
float h() const { return 3.0; }
};
class Derived : Base1 { // Private inheritance
public:
Base1::f; // Name publicizes member
Base1::h;
};
int main() {
Derived d;
d.f();
d.h();
//! d.g(); // Error -- private function
} ///:~
Thus,
private
inheritance is useful if you want to hide part of the functionality of the base
class.
You
should think carefully before using
private
inheritance instead of member objects;
private
inheritance has particular complications when combined with run-time type
identification (the subject of Chapter 17).
Go to CodeGuru.com
Contact: webmaster@codeguru.com
© Copyright 1997-1999 CodeGuru