|
|
|
|
Creating an object-based hierarchy
An issue that has been recurring
throughout this book during the demonstration of the container classes
Stack and Stash is the “ownership problem.” The
“owner” refers to who or what is responsible for calling delete
for objects that have been created dynamically (using new). The
problem when using containers is that they need to be flexible enough to hold
different types of objects. To do this, the containers have held void
pointers and so they haven’t known the type of object they’ve held.
Deleting a void pointer doesn’t call the destructor, so the
container couldn’t be responsible for cleaning up its
objects.
One solution was presented in the example
C14:InheritStack.cpp, in which the Stack was inherited into a new
class that accepted and produced only string pointers. Since it knew that
it could hold only pointers to string objects, it could properly delete
them. This was a nice solution, but it requires you to inherit a new container
class for each type that you want to hold in the container. (Although this seems
tedious now, it will actually work quite well in Chapter 16, when templates are
introduced.)
The problem is that you want the
container to hold more than one type, but you don’t want to use
void pointers. Another solution is to use polymorphism by forcing all the
objects held in the container to be inherited from the same base class. That is,
the container holds the objects of the base class, and then you can call virtual
functions – in particular, you can call virtual destructors to solve the
ownership problem.
This
solution uses what is referred to as a singly-rooted hierarchy or an
object-based hierarchy (because the root class of the hierarchy is
usually named “Object”). It turns out that there are many other
benefits to using a singly-rooted hierarchy; in fact, every other
object-oriented language but C++ enforces the use of such a hierarchy –
when you create a class, you are automatically inheriting it directly or
indirectly from a common base class, a base class that was established by the
creators of the language. In C++, it was thought that the enforced use of this
common base class would cause too much overhead, so it was left out. However,
you can choose to use a common base class in your own projects, and this subject
will be examined further in Volume 2 of this book.
To solve the ownership problem, we can
create an extremely simple Object for the base class, which contains only
a virtual destructor. The Stack can then hold classes inherited from
Object:
//: C15:OStack.h
// Using a singly-rooted hierarchy
#ifndef OSTACK_H
#define OSTACK_H
class Object {
public:
virtual ~Object() = 0;
};
// Required definition:
inline Object::~Object() {}
class Stack {
struct Link {
Object* data;
Link* next;
Link(Object* dat, Link* nxt) :
data(dat), next(nxt) {}
}* head;
public:
Stack() : head(0) {}
~Stack(){
while(head)
delete pop();
}
void push(Object* dat) {
head = new Link(dat, head);
}
Object* peek() const {
return head ? head->data : 0;
}
Object* pop() {
if(head == 0) return 0;
Object* result = head->data;
Link* oldHead = head;
head = head->next;
delete oldHead;
return result;
}
};
#endif // OSTACK_H ///:~
To simplify things by keeping everything
in the header file, the (required) definition for the pure virtual destructor is
inlined into the header file, and pop( ) (which might be considered
too large for inlining) is also inlined.
Link
objects now hold pointers to Object rather than void pointers, and
the Stack will only accept and return Object pointers. Now
Stack is much more flexible, since it will hold lots of different types
but will also destroy any objects that are left on the Stack. The new
limitation (which will be finally removed when templates are applied to the
problem in Chapter 16) is that anything that is placed on the Stack must
be inherited from Object. That’s fine if you are starting your
class from scratch, but what if you already have a class such as string
that you want to be able to put onto the Stack? In this case, the new
class must be both a string and an Object, which means it must be
inherited from both classes. This is called multiple inheritance and it
is the subject of an entire chapter in Volume 2 of this book (downloadable from
www.BruceEckel.com). When you read that chapter, you’ll see that
multiple inheritance can be fraught with complexity, and is a feature you should
use sparingly. In this situation, however, everything is simple enough that we
don’t trip across any multiple inheritance pitfalls:
//: C15:OStackTest.cpp
//{T} OStackTest.cpp
#include "OStack.h"
#include "../require.h"
#include <fstream>
#include <iostream>
#include <string>
using namespace std;
// Use multiple inheritance. We want
// both a string and an Object:
class MyString: public string, public Object {
public:
~MyString() {
cout << "deleting string: " << *this << endl;
}
MyString(string s) : string(s) {}
};
int main(int argc, char* argv[]) {
requireArgs(argc, 1); // File name is argument
ifstream in(argv[1]);
assure(in, argv[1]);
Stack textlines;
string line;
// Read file and store lines in the stack:
while(getline(in, line))
textlines.push(new MyString(line));
// Pop some lines from the stack:
MyString* s;
for(int i = 0; i < 10; i++) {
if((s=(MyString*)textlines.pop())==0) break;
cout << *s << endl;
delete s;
}
cout << "Letting the destructor do the rest:"
<< endl;
} ///:~
Although this is similar to the previous
version of the test program for Stack, you’ll notice that only 10
elements are popped from the stack, which means there are probably some objects
remaining. Because the Stack knows that it holds Objects, the
destructor can properly clean things up, and you’ll see this in the output
of the program, since the MyString objects print messages as they are
destroyed.
Creating containers that hold
Objects is not an unreasonable approach – if you have a
singly-rooted hierarchy (enforced either by the language or by the requirement
that every class inherit from Object). In that case, everything is
guaranteed to be an Object and so it’s not very complicated to use
the containers. In C++, however, you cannot expect this from every class, so
you’re bound to trip over multiple inheritance if you take this approach.
You’ll see in Chapter 16 that templates solve the problem in a much
simpler and more elegant
fashion.
|
|
|