Bruce Eckel's Thinking in C++, 2nd Ed Contents | Prev | Next

RTTI considered harmful?

Various designs in this chapter attempt to remove RTTI, which might give you the impression that it’s “considered harmful” (the condemnation used for poor goto). This isn’t true; it is the misuse of RTTI that is the problem. The reason our designs removed RTTI is because the misapplication of that feature prevented extensibility, which contravened the stated goal of adding a new type to the system with as little impact on surrounding code as possible. Since RTTI is often misused by having it look for every single type in your system, it causes code to be non-extensible: when you add a new type, you have to go hunting for all the code in which RTTI is used, and if you miss any you won’t get help from the compiler.

However, RTTI doesn’t automatically create non-extensible code. Let’s revisit the trash recycler once more. This time, a new tool will be introduced, which I call a TypeMap. It inherits from a map that holds a variant of type_info object as the key, and vector<Trash*> as the value. The interface is simple: you call addTrash( ) to add a new Trash pointer, and the map class provides the rest of the interface. The keys represent the types contained in the associated vector. The beauty of this design (suggested by Larry O’Brien) is that the TypeMap dynamically adds a new key-value pair whenever it encounters a new type, so whenever you add a new type to the system (even if you add the new type at runtime), it adapts.

The example will again build on the structure of the Trash types, and will use fillBin( ) to parse and insert the values into the TypeMap. However, TypeMap is not a vector<Trash*>, and so it must be adapted to work with fillBin( ) by multiply inheriting from Fillable. In addition, the Standard C++ type_info class is too restrictive to be used as a key, so a kind of wrapper class TypeInfo is created, which simply extracts and stores the type_info char* representation of the type (making the assumption that, within the realm of a single compiler, this representation will be unique for each type).

//: C25:DynaTrash.cpp
//{L} TrashPrototypeInit
//{L} fillBin Trash TrashStatics
// Using a map of vectors and RTTI
// to automatically sort Trash into
// vectors. This solution, despite the
// use of RTTI, is extensible.
#include "Trash.h"
#include "fillBin.h"
#include "sumValue.h"
#include "../purge.h"
#include <iostream>
#include <fstream>
#include <vector>
#include <map>
#include <typeinfo>
using namespace std;
ofstream out("DynaTrash.out");

// Must adapt from type_info in Standard C++,
// since type_info is too restrictive:
template<class T> // T should be a base class
class TypeInfo {
  string id;
public:
  TypeInfo(T* t) : id(typeid(*t).name()) {}
  const string& name() { return id; }
  friend bool operator<(const TypeInfo& lv,
    const TypeInfo& rv){
    return lv.id < rv.id;
  }
};

class TypeMap : 
  public map<TypeInfo<Trash>, vector<Trash*> >,
  public Fillable {
public:
  // Satisfies the Fillable interface:
  void addTrash(Trash* t) {
    (*this)[TypeInfo<Trash>(t)].push_back(t);
  }
  ~TypeMap() {
    for(iterator it = begin(); it != end(); it++)
      purge((*it).second);
  }
};

int main() {
  TypeMap bin;
  fillBin("Trash.dat", bin); // Sorting happens
  TypeMap::iterator it;
  for(it = bin.begin(); it != bin.end(); it++)
    sumValue((*it).second);
} ///:~

TypeInfo is templatized because typeid( ) does not allow the use of void*, which would be the most general way to solve the problem. So you are required to work with some specific class, but this class should be the most base of all the classes in your hierarchy. TypeInfo must define an operator< because a map needs it to order its keys.

Although powerful, the definition for TypeMap is simple; the addTrash( ) member function does most of the work. When you add a new Trash pointer, the a TypeInfo<Trash> object for that type is generated. This is used as a key to determine whether a vector holding objects of that type is already present in the map. If so, the Trash pointer is added to that vector. If not, the TypeInfo object and a new vector are added as a key-value pair.

An iterator to the map, when dereferenced, produces a pair object where the key (TypeInfo) is the first member, and the value ( Vector<Trash*>) is the second member. And that’s all there is to it.

The TypeMap takes advantage of the design of fillBin( ), which doesn’t just try to fill a vector but instead anything that implements the Fillable interface with its addTrash( ) member function. Since TypeMap is multiply inherited from Fillable, it can be used as an argument to fillBin( ) like this:

fillBin("Trash.dat", bin);

An interesting thing about this design is that even though it wasn’t created to handle the sorting, fillBin( ) is performing a sort every time it inserts a Trash pointer into bin. When the Trash is thrown into bin it’s immediately sorted by TypeMap’s internal sorting mechanism. Stepping through the TypeMap and operating on each individual vector becomes a simple matter, and uses ordinary STL syntax.

As you can see, adding a new type to the system won’t affect this code at all, nor the code in TypeMap. This is certainly the smallest solution to the problem, and arguably the most elegant as well. It does rely heavily on RTTI, but notice that each key-value pair in the map is looking for only one type. In addition, there’s no way you can “forget” to add the proper code to this system when you add a new type, since there isn’t any code you need to add, other than that which supports the prototyping process (and you’ll find out right away if you forget that).

Contents | Prev | Next


Contact: webmaster@codeguru.com
CodeGuru - the website for developers.
[an error occurred while processing this directive]