Doug Lea
Computer Science, SUNY Oswego, Oswego NY 13126
dl@cs.oswego.edu
(Written December 1991. Published in Usenix C++ Conference Proceedings 1992. Formatted in HTML April 1997.)
Abstract: Design applications of the run-time type identification constructs proposed by Stroustrup and Lenkov are illustrated via several examples that demonstrate their strengths and weaknesses as tools in object-oriented design.
Some people think that run-time type identification (RTTI) constructs cause programmers to sidestep many of the good design practices evident in well-crafted object-oriented programs. Others think that it is impossible to even write well-crafted object-oriented programs without run-time type support. This commentary paper briefly attempts to disentangle some of the issues behind such views. A few problems are illustrated for which RTTI might plausibly be used to help formulate a solution. These lead to a discussion of some underlying design and engineering considerations, and allow some tentative conclusions (noted in bold throughout the paper).
The first example involves probably the most common application of RTTI. Assume a base class, along with a subclass that possesses additional properties not listed in the base. For example:
class Person { ... }; class Employee : public Person { public: virtual float salary() const; virtual Department* dept(); ... };
Along with a heterogeneous collection class, for example:
class PersonList { public: Person* first(); Person* next(Person*); ... };
And finally the problem:
Write sumSalaries(PersonList* l), that returns the sum of all salaries in l.
This is an impossible demand, since PersonList entries don't necessarily possess salary attributes. At best, we can sum the salaries for all (sub) Persons known to contain a salary. In doing so, we might arbitrarily decide to treat all others as having a salary of zero. Given this, a solution may be had using RTTI:
float sumSalaries(PersonList* l) { float sum = 0.0; for (Person* p = l->first(); p != 0; p = l->next(p)) if (Employee* e = (Employee*)p) sum += e->salary(); return sum; }
The sumSalaries procedure might be seen as implicitly attaching a new property to class Person, namely:
class Person { ... virtual float salary() const { return 0.0; } };
The fact that this was done implicitly seems innocent enough. But what if some other procedure having to do with salaries and persons made a different decision; e.g., that unless specified, the salary of a Person should be estimated as the average yearly per capita income? This is the sort of software management problem that classes, encapsulation, and inheritance were meant to solve, not create. And this is the sort of usage that gives RTTI a bad reputation.
It would have been much better to build the default salary attribute into Person to begin with. But perhaps the class ``belongs'' to someone else and cannot be changed. Perhaps changes would break other existing code. There are many such reasons for not touching class interfaces when you don't absolutely have to. There is a nicer-looking solution. It may be approached through a version that looks even nicer still, but does not work as naively expected:
float getSalary(Person* p) { return 0.0; } // wrong float getSalary(Employee* p) { return p->salary(); } float sumSalaries_2(PersonList* l) { float sum = 0.0; for (Person* p = l->first(); p != 0; p = l->next(p)) sum += getSalary(p); return sum; }
This is better than the original version, since the decision to treat non-existing salaries as zero is clearly enshrined within independent procedures that all other classes and procedures may use.
Unfortunately, the code does not solve the problem. C++ does not dynamically dispatch procedures on the basis of arguments, only receivers (and and only when declared virtual, etc.). Thus sumSalaries_2 would always return zero. However, this can be fixed using RTTI:
float getSalary(Person* p) { Employee* e; if (e = (Employee*)p) return e->salary(); else return 0.0; }
All is well with the above solution until the day someone adds:
class Contractor : public Person { ... virtual float salary() const; virtual Job* job(); };
Contractors aren't Employees, yet they also have salary attributes. If a Contractor ever shows up in a PersonList, then both sumSalaries and sumSalaries_2 will treat its salary as zero. This is probably not what anyone had in mind.
The problem is that the class name Employee was an alias for possession of the property (method) salary. This trick works only when it works.
In the current example there are several cures, including:
Each of these has its merits. Each also requires changes to existing code after the introduction of Contractor. RTTI does not always eliminate the need for such alterations.
Note however that any of these strategies could have been applied in our original versions. People tend not to do so though. Routine creation of extremely fine-grained classes corresponding to each ``added'' property gets pretty tedious, as does the alternative of routinely extracting Type_info information probing for possession of these properties. These human-factors considerations are sometimes serious barriers to extensibility. RTTI offers an incomplete solution. (Other equally incomplete solutions include views [5] and conformance based typing [4].)
This example was made famous in a set of Usenet postings:
class Driver { ... }; class ProDriver : public Driver { ... }; class Vehicle { ... virtual void Register(Driver* d) { vd(); } }; class Truck : public Vehicle { ... void Register(ProDriver* d) { tp(); } };
The idea here seems to be that a Vehicle may be registered to any kind of Driver, but a Truck may only be registered (in some perhaps different way, as signified by tp() vs vd()) to a ProDriver (professional driver).
The above declarations are not quite illegal C++ (see [2] chapter 13 for the gruesome details) but do not work as expected. For example,
void reg(Vehicle* v, Driver* d) { v.Register(d); } main() { Truck* t = new Truck; ProDriver* p = new ProDriver; t.Register(p); // Truck::Register(ProDriver*) invoked reg(t, p); // Vehicle::Register(Driver*) invoked via reg }
This is a more subtle consequence of C++ rules that dynamically dispatch only on receiver, not argument types.
A cure may be obtained by ``pulling out'' the Register method from the classes and using RTTI.
void Register(Vehicle* v, Driver* d) { if ((Truck*)(v) && (ProDriver*)(p)) tp(); else vd(); }
This style of specialization based on the types of (potentially) ALL participants in an operation is called multimethod dispatching. CLOS [1] is justly famous for supporting multimethods as first-class programming constructs.
(Footnote:The remarks in [6] about specifically not including multimethods in their proposal seem misplaced given that many uses of RTTI amount to their simulation. The main difference and advantage of first-class multimethods is that they are extensible -- new special cases may be added without modifying existing code. In any case, multimethods and RTTI can each fully simulate the other.If C++ supported multimethods directly, then this might have been written somewhat more clearly and extensibly:
void Register(Vehicle* v, Driver* d) { vd(); } void Register(Truck* v, ProDriver* d) { tp(); }
The simulated multimethod solution has the advantage of predictable dispatching. This is vital in order to statically determine correctness, or even reasonableness. It's hard to say very much at all about the original version. In practice, using RTTI-simulated multimethods to control dispatching of special cases of overloaded methods and procedures is much safer and more reliable than depending on C++ ``overload resolution'' policies.
But in the current example, the improved clarity highlights conceptual problems with the design. The probable intent was to disallow all but ProDrivers from registering Trucks. The above solution allows Drivers to register them, but uses the vd() code in Vehicle::Register to do so. The use of multimethod dispatch seems like the wrong way to address this. It uses type information to direct, not guard or prohibit certain calls.
In these kinds of designs, there is simply no way to statically prohibit certain argument combinations. The special cases must be considered truly exceptional to the general Vehicle- Driver relationship. Probably the best solution here would be to explicitly indicate possible failure to clients. This could be done in several ways, including:
bool Register(Vehicle* v, Driver* d) { if ((Truck*)(v)) if ((ProDriver*)(p)) { tp(); return TRUE; } else return FALSE; else { vd(); return TRUE; } }
Suppose we are building a class representing face icons that may be in any of three states, happy, sad, and asleep. One design strategy is to create three different classes, one per state and a ``controller'', that switches among them. This is an attractive delegation [3] based design:
class IconState { virtual bool eyesOpen() const = 0; ... }; class HappyIcon : public IconState { bool eyesOpen() const { return TRUE; } ... }; class AsleepIcon : public IconState { ... } class SadIcon : public IconState { ... } class Icon { IconState* theIcon; virtual bool eyesOpen const { return theIcon->eyesOpen(); } ... virtual bool isHappy const { return ((HappyIcon*)(theIcon) != 0); } };
This is a situation in which RTTI is clearly the best alternative. How else would an Icon know which state it were in within isHappy? Alternatives like maintaining logical variables invite needless error-prone complexity.
Importantly, this strategy extends to testing and internal integrity checking. For example, the Icon class requires a beHappy method to change state:
... virtual void beHappy() { theIcon = getHappyIcon(); assert(isHappy()); }
The main reason this works so nicely here is that we have carefully merged the notions of class membership and property (and/or property value) possession. This takes some planning.
Suppose we need to design a long-lived application program with fault-tolerance support in case of crashes. We settle on a checkpoint/rollback scheme in which the states of all objects are periodically saved on disk. Recovery is performed by re-constructing or reinitializing (depending on the nature of the crash) all objects to their last saved states.
Design and implementation of such mechanisms is not an easy matter. Doing a thorough job is tantamount to the construction of an object-oriented database system. But there is the widespread belief that RTTI substantially simplifies practical application-specific solutions.
``Substantially'' is much too strong a term here. Most of the snags in this kind of persistence support revolve around the transformation and associated bookkeeping of object identities, that are internally represented through pointer values, but externally through some other scheme (e.g., integer pseudo-identities). Class identities must also be stored and recovered in order to allow reconstruction. RTTI per se can only assist in only the latter.
On the other hand, RTTI certainly doesn't make this any harder. There are many ways to design a save/restore mechanism. Here is a simplified prototypical framework (For example, among the simplifications is that it does not accommodate ``embedded'' objects; i.e., those that directly nest one object within another.) On the save side, for each object to be stored:
The restore side is mostly symmetrical. For each object to be recovered:
There are a number of ways in which RTTI can make these tasks a bit easier to implement, without otherwise affecting their logic one way or the other. These mainly arise through exploitation of extensible Type_info structures.
It should soon occur to anyone familiar with languages like Smalltalk that the logic of grouping these kinds of per-class bookkeeping routines in a central place leads to the notion of metaclasses as a replacement for C++-style static class functions including, significantly, client-accessible constructors. This may in turn lead to a very different style of class design in general -- for many purposes, typeid(p).info() might as well be pronounced ``p's metaclass''.