MindView Inc.
[ Viewing Hints ] [ Revision History ] [ Free Newsletter ]
[ Seminars ] [ Seminars on CD ROM ] [ Consulting ]

Thinking in C++, 2nd ed., Volume 2, Revision 2

©2000 by Bruce Eckel

[ Previous Chapter ] [ Short TOC ] [ Table of Contents ] [ Index ] [ Next Chapter ]

8: Run-time type identification

Run-time type identification (RTTI) lets you find the exact type of an object when you have only a pointer or reference to the base type.

This can be thought of as a “secondary” feature in C++, a pragmatism to help out when you get into messy situations. Normally, you’ll want to intentionally ignore the exact type of an object and let the virtual function mechanism implement the correct behavior for that type. But occasionally it’s useful to know the exact type of an object for which you only have a base pointer. Often this information allows you to perform a special-case operation more efficiently or prevent a base-class interface from becoming ungainly. It happens enough that most class libraries contain virtual functions to produce run-time type information. When exception handling was added to C++, it required the exact type information about objects. It became an easy next step to build access to that information into the language.

This chapter explains what RTTI is for and how to use it. In addition, it explains the why and how of the new C++ cast syntax, which has the same appearance as RTTI.

The “Shape” example

This is an example of a class hierarchy that uses polymorphism. The generic type is the base class Shape, and the specific derived types are Circle, Square, and Triangle:


This is a typical class-hierarchy diagram, with the base class at the top and the derived classes growing downward. The normal goal in object-oriented programming is for the bulk of your code to manipulate pointers to the base type (Shape, in this case) so if you decide to extend the program by adding a new class (rhomboid, derived from Shape, for example), the bulk of the code is not affected. In this example, the virtual function in the Shape interface is draw( ), so the intent is for the client programmer to call draw( ) through a generic Shape pointer. draw( ) is redefined in all the derived classes, and because it is a virtual function, the proper behavior will occur even though it is called through a generic Shape pointer.

Thus, you generally create a specific object (Circle, Square, or Triangle), take its address and cast it to a Shape* (forgetting the specific type of the object), and use that anonymous pointer in the rest of the program. Historically, diagrams are drawn as seen above, so the act of casting from a more derived type to a base type is called upcasting.

What is RTTI?

But what if you have a special programming problem that’s easiest to solve if you know the exact type of a generic pointer? For example, suppose you want to allow your users to highlight all the shapes of any particular type by turning them purple. This way, they can find all the triangles on the screen by highlighting them. Your natural first approach may be to try a virtual function like TurnColorIfYouAreA( ), which allows enumerated arguments of some type color and of Shape::Circle, Shape::Square, or Shape::Triangle.

To solve this sort of problem, most class library designers put virtual functions in the base class to return type information about the specific object at runtime. You may have seen library member functions with names like isA( ) and typeOf( ). These are vendor-defined RTTI functions. Using these functions, as you go through the list you can say, “If you’re a triangle, turn purple.”

When exception handling was added to C++, the implementation required that some run-time type information be put into the virtual function tables. This meant that with a small language extension the programmer could also get the run-time type information about an object. All library vendors were adding their own RTTI anyway, so it was included in the language.

RTTI, like exceptions, depends on type information residing in the virtual function table. If you try to use RTTI on a class that has no virtual functions, you’ll get unexpected results.

Two syntaxes for RTTI

There are two different ways to use RTTI. The first acts like sizeof( ) because it looks like a function, but it’s actually implemented by the compiler. typeid( ) takes an argument that’s an object, a reference, or a pointer and returns a reference to a global const object of type typeinfo. These can be compared to each other with the operator== and operator!=, and you can also ask for the name( ) of the type, which returns a string representation of the type name. Note that if you hand typeid( ) a Shape*, it will say that the type is Shape*, so if you want to know the exact type it is pointing to, you must dereference the pointer. For example, if s is a Shape*,

cout << typeid(*s).name() << endl;

will print out the type of the object s points to.

You can also ask a typeinfo object if it precedes another typeinfo object in the implementation-defined “collation sequence,” using before(typeinfo&), which returns true or false. When you say,

if(typeid(me).before(typeid(you))) // ...

you’re asking if me occurs before you in the collation sequence.

The second syntax for RTTI is called a “type-safe downcast.” The reason for the term “downcast” is (again) the historical arrangement of the class hierarchy diagram. If casting a Circle* to a Shape* is an upcast, then casting a Shape* to a Circle* is a downcast. However, you know a Circle* is also a Shape*,and the compiler freely allows an upcast assignment, but you don’t know that a Shape* is necessarily a Circle*, so the compiler doesn’t allow you to perform a downcast assignment without using an explicit cast. You can of course force your way through using ordinary C-style casts or a C++ static_cast (described at the end of this chapter), which says, “I hope this is actually a Circle*, and I’m going to pretend it is.” Without some explicit knowledge that it is in fact a Circle, this is a totally dangerous thing to do. A common approach in vendor-defined RTTI is to create some function that attempts to assign (for this example) a Shape* to a Circle*, checking the type in the process. If this function returns the address, it was successful; if it returns null, you didn’t have a Circle*.

The C++ RTTI typesafe-downcast follows this “attempt-to-cast” function form, but it uses (very logically) the template syntax to produce the special function dynamic_cast. So the example becomes

Shape* sp = new Circle;
Circle* cp = dynamic_cast<Circle*>(sp);
if(cp) cout << "cast successful";

The template argument for dynamic_cast is the type you want the function to produce, and this is the return value for the function. The function argument is what you are trying to cast from.

Normally you might be hunting for one type (triangles to turn purple, for instance), but the following example fragment can be used if you want to count the number of various shapes.

  Circle* cp = dynamic_cast<Circle*>(sh);
  Square* sp = dynamic_cast<Square*>(sh);
  Triangle* tp = dynamic_cast<Triangle*>(sh);


Of course this is contrived – you’d probably put a static data member in each type and increment it in the constructor. You would do something like that if you had control of the source code for the class and could change it. Here’s an example that counts shapes using both the static member approach and dynamic_cast:

//: C08:Rtshapes.cpp
// Counting shapes
#include "../purge.h"
#include <iostream>
#include <ctime>
#include <typeinfo>
#include <vector>
using namespace std;

class Shape {
protected:
  static int count;
public:
  Shape() { count++; }
  virtual ~Shape() { count--; }
  virtual void draw() const = 0;
  static int quantity() { return count; }
};

int Shape::count = 0;

class SRectangle : public Shape {
  void operator=(SRectangle&); // Disallow
protected:
  static int count;
public:
  SRectangle() { count++; }
  SRectangle(const SRectangle&) { count++;}
  ~SRectangle() { count--; }
  void draw() const {
    cout << "SRectangle::draw()" << endl;
  }
  static int quantity() { return count; }
};

int SRectangle::count = 0;

class SEllipse : public Shape {
  void operator=(SEllipse&); // Disallow
protected:
  static int count;
public:
  SEllipse() { count++; }
  SEllipse(const SEllipse&) { count++; }
  ~SEllipse() { count--; }
  void draw() const {
    cout << "SEllipse::draw()" << endl;
  }
  static int quantity() { return count; }
};

int SEllipse::count = 0;

class SCircle : public SEllipse {
  void operator=(SCircle&); // Disallow
protected:
  static int count;
public:
  SCircle() { count++; }
  SCircle(const SCircle&) { count++; }
  ~SCircle() { count--; }
  void draw() const {
    cout << "SCircle::draw()" << endl;
  }
  static int quantity() { return count; }
};

int SCircle::count = 0;

int main() {
  vector<Shape*> shapes;
  srand(time(0)); // Seed random number generator
  const int mod = 12;
  // Create a random quantity of each type:
  for(int i = 0; i < rand() % mod; i++)
    shapes.push_back(new SRectangle);
  for(int j = 0; j < rand() % mod; j++)
    shapes.push_back(new SEllipse);
  for(int k = 0; k < rand() % mod; k++)
    shapes.push_back(new SCircle);
  int nCircles = 0;
  int nEllipses = 0;
  int nRects = 0;
  int nShapes = 0;
  for(int u = 0; u < shapes.size(); u++) {
    shapes[u]->draw();
    if(dynamic_cast<SCircle*>(shapes[u]))
      nCircles++;
    if(dynamic_cast<SEllipse*>(shapes[u]))
      nEllipses++;
    if(dynamic_cast<SRectangle*>(shapes[u]))
      nRects++;
    if(dynamic_cast<Shape*>(shapes[u]))
      nShapes++;
  }
  cout << endl << endl
    << "Circles = " << nCircles << endl
    << "Ellipses = " << nEllipses << endl
    << "Rectangles = " << nRects << endl
    << "Shapes = " << nShapes << endl
    << endl
    << "SCircle::quantity() = "
    << SCircle::quantity() << endl
    << "SEllipse::quantity() = "
    << SEllipse::quantity() << endl
    << "SRectangle::quantity() = "
    << SRectangle::quantity() << endl
    << "Shape::quantity() = "
    << Shape::quantity() << endl;
  purge(shapes);
} ///:~

Both types work for this example, but the static member approach can be used only if you own the code and have installed the static members and functions (or if a vendor provides them for you). In addition, the syntax for RTTI may then be different from one class to another.

Syntax specifics

This section looks at the details of how the two forms of RTTI work, and how they differ.

typeid( ) with built-in types

For consistency, the typeid( ) operator works with built-in types. So the following expressions are true:

//: C08:TypeidAndBuiltins.cpp
#include <cassert>
#include <typeinfo>
using namespace std;

int main() {
  assert(typeid(47) == typeid(int));
  assert(typeid(0) == typeid(int));
  int i;
  assert(typeid(i) == typeid(int));
  assert(typeid(&i) == typeid(int*));
} ///:~


Producing the proper type name

typeid( ) must work properly in all situations. For example, the following class contains a nested class:

//: C08:RTTIandNesting.cpp
#include <iostream>
#include <typeinfo>
using namespace std;

class One {
  class Nested {};
  Nested* n;
public:
  One() : n(new Nested) {}
  ~One() { delete n; }
  Nested* nested() { return n; }
};

int main() {
  One o;
  cout << typeid(*o.nested()).name() << endl;
} ///:~

The typeinfo::name( ) member function will still produce the proper class name; the result is One::Nested.

Nonpolymorphic types

Although typeid( ) works with nonpolymorphic types (those that don’t have a virtual function in the base class), the information you get this way is dubious. For the following class hierarchy,

//: C08:RTTIWithoutPolymorphism.cpp
#include <cassert>
#include <typeinfo>
using namespace std;

class X {
 int i; 
public:
  // ...
};

class Y : public X { 
  int j;
public:
  // ...
};

int main() {
  X* xp = new Y;
  assert(typeid(*xp) == typeid(X));
  assert(typeid(*xp) != typeid(Y));
} ///:~

If you create an object of the derived type and upcast it,

X* xp = new Y;

The typeid( ) operator will produce results, but not the ones you might expect. Because there’s no polymorphism, the static type information is used:

typeid(*xp) == typeid(X)
typeid(*xp) != typeid(Y)

RTTI is intended for use only with polymorphic classes.

Casting to intermediate levels

dynamic_cast can detect both exact types and, in an inheritance hierarchy with multiple levels, intermediate types. For example,

//: C08:DynamicCast.cpp
// Using the standard dynamic_cast operation
#include <cassert>
#include <typeinfo>
using namespace std;

class D1 { 
public:
  virtual void func() {} 
  virtual ~D1() {}
};

class D2 { 
public:
  virtual void bar() {} 
};

class MI : public D1, public D2 {};
class Mi2 : public MI {};

int main() {
  D2* d2 = new Mi2;
  Mi2* mi2 = dynamic_cast<Mi2*>(d2);
  MI* mi = dynamic_cast<MI*>(d2);
  D1* d1 = dynamic_cast<D1*>(d2);
  assert(typeid(d2) != typeid(Mi2*));
  assert(typeid(d2) == typeid(D2*));
} ///:~

This has the extra complication of multiple inheritance. If you create an mi2 and upcast it to the root (in this case, one of the two possible roots is chosen), then the dynamic_cast back to either of the derived levels MI or mi2 is successful.

You can even cast from one root to the other:

  D1* d1 = dynamic_cast<D1*>(d2);

This is successful because D2 is actually pointing to an mi2 object, which contains a subobject of type d1.

Casting to intermediate levels brings up an interesting difference between dynamic_cast and typeid( ). typeid( ) always produces a reference to a typeinfo object that describes the exact type of the object. Thus it doesn’t give you intermediate-level information. In the following expression (which is true), typeid( ) doesn’t see d2 as a pointer to the derived type, like dynamic_cast does:

typeid(d2) != typeid(Mi2*)

The type of D2 is simply the exact type of the pointer:

typeid(d2) == typeid(D2*)


void pointers

Run-time type identification doesn’t work with void pointers:

//: C08:Voidrtti.cpp
// RTTI & void pointers
#include <iostream>
#include <typeinfo>
using namespace std;

class Stimpy {
public:
  virtual void happy() {}
  virtual void joy() {}
  virtual ~Stimpy() {}
};

int main() {
  void* v = new Stimpy;
  // Error:
//!  Stimpy* s = dynamic_cast<Stimpy*>(v);
  // Error:
//!  cout << typeid(*v).name() << endl;
} ///:~

A void* truly means “no type information at all.”

Using RTTI with templates

Templates generate many different class names, and sometimes you’d like to print out information about what class you’re in. RTTI provides a convenient way to do this. The following example revisits the code in Chapter XX to print out the order of constructor and destructor calls without using a preprocessor macro:

//: C08:ConstructorOrder.cpp
// Order of constructor calls
#include <iostream>
#include <typeinfo>
using namespace std;

template<int id> class Announce {
public:
  Announce() {
    cout << typeid(*this).name()
         << " constructor " << endl;
  }
  ~Announce() {
    cout << typeid(*this).name()
         << " destructor " << endl;
  }
};

class X : public Announce<0> {
  Announce<1> m1;
  Announce<2> m2;
public:
  X() { cout << "X::X()" << endl; }
  ~X() { cout << "X::~X()" << endl; }
};

int main() { X x; } ///:~

The <typeinfo> header must be included to call any member functions for the typeinfo object returned by typeid( ). The template uses a constant int to differentiate one class from another, but class arguments will work as well. Inside both the constructor and destructor, RTTI information is used to produce the name of the class to print. The class X uses both inheritance and composition to create a class that has an interesting order of constructor and destructor calls.

This technique is often useful in situations when you’re trying to understand how the language works.

References

RTTI must adjust somewhat to work with references. The contrast between pointers and references occurs because a reference is always dereferenced for you by the compiler, whereas a pointer’s type or the type it points to may be examined. Here’s an example:

//: C08:RTTIwithReferences.cpp
#include <cassert>
#include <typeinfo>
using namespace std;

class B {
public:
  virtual float f() { return 1.0;}
  virtual ~B() {}
};

class D : public B { /* ... */ };

int main() {
  B* p = new D;
  B& r = *p;
  assert(typeid(p) == typeid(B*));
  assert(typeid(p) != typeid(D*));
  assert(typeid(r) == typeid(D));
  assert(typeid(*p) == typeid(D));
  assert(typeid(*p) != typeid(B));
  assert(typeid(&r) == typeid(B*));
  assert(typeid(&r) != typeid(D*));
  assert(typeid(r.f()) == typeid(float));
} ///:~

Whereas the type of pointer that typeid( ) sees is the base type and not the derived type, the type it sees for the reference is the derived type:

typeid(p) == typeid(B*)
typeid(p) != typeid(D*)
typeid(r) == typeid(D)

Conversely, what the pointer points to is the derived type and not the base type, and taking the address of the reference produces the base type and not the derived type:

typeid(*p) == typeid(D)
typeid(*p) != typeid(B)
typeid(&r) == typeid(B*)
typeid(&r) != typeid(D*)

Expressions may also be used with the typeid( ) operator because they have a type as well:

typeid(r.f()) == typeid(float)


Exceptions

When you perform a dynamic_cast to a reference, the result must be assigned to a reference. But what happens if the cast fails? There are no null references, so this is the perfect place to throw an exception; the Standard C++ exception type is bad_cast, but in the following example the ellipses are used to catch any exception:

//: C08:RTTIwithExceptions.cpp
#include <typeinfo>
#include <iostream>
using namespace std;
class X { public: virtual ~X(){} };
class B { public: virtual ~B(){} };
class D : public B {};

int main() {
  D d;
  B & b = d; // Upcast to reference
  try {
    X& xr = dynamic_cast<X&>(b);
  } catch(...) {
    cout << "dynamic_cast<X&>(b) failed" 
         << endl;
  }
  X* xp = 0;
  try {
    typeid(*xp); // Throws exception
  } catch(bad_typeid) {
    cout << "Bad typeid() expression" << endl;
  }
} ///:~

The failure, of course, is because b doesn’t actually point to an X object. If an exception was not thrown here, then xr would be unbound, and the guarantee that all objects or references are constructed storage would be broken.

An exception is also thrown if you try to dereference a null pointer in the process of calling typeid( ). The Standard C++ exception is called bad_typeid.

Here (unlike the reference example above) you can avoid the exception by checking for a nonzero pointer value before attempting the operation; this is the preferred practice.

Multiple inheritance

Of course, the RTTI mechanisms must work properly with all the complexities of multiple inheritance, including virtual base classes:

//: C08:RTTIandMultipleInheritance.cpp
#include <iostream>
#include <typeinfo>
using namespace std;

class BB {
public:
  virtual void f() {}
  virtual ~BB() {}
};
class B1 : virtual public BB {};
class B2 : virtual public BB {};
class MI : public B1, public B2 {};

int main() {
  BB* bbp = new MI; // Upcast
  // Proper name detection:
  cout << typeid(*bbp).name() << endl;
  // Dynamic_cast works properly:
  MI* mip = dynamic_cast<MI*>(bbp);
  // Can't force old-style cast:
  //! MI* mip2 = (MI*)bbp; // Compile error
} ///:~

typeid( ) properly detects the name of the actual object, even through the virtual base class pointer. The dynamic_cast also works correctly. But the compiler won’t even allow you to try to force a cast the old way:

MI* mip = (MI*)bbp; // Compile-time error

It knows this is never the right thing to do, so it requires that you use a dynamic_cast.

Sensible uses for RTTI

Because it allows you to discover type information from an anonymous polymorphic pointer, RTTI is ripe for misuse by the novice because RTTI may make sense before virtual functions do. For many people coming from a procedural background, it’s very difficult not to organize their programs into sets of switch statements. They could accomplish this with RTTI and thus lose the very important value of polymorphism in code development and maintenance. The intent of C++ is that you use virtual functions throughout your code, and you only use RTTI when you must.

However, using virtual functions as they are intended requires that you have control of the base-class definition because at some point in the extension of your program you may discover the base class doesn’t include the virtual function you need. If the base class comes from a library or is otherwise controlled by someone else, a solution to the problem is RTTI: You can inherit a new type and add your extra member function. Elsewhere in the code you can detect your particular type and call that member function. This doesn’t destroy the polymorphism and extensibility of the program, because adding a new type will not require you to hunt for switch statements. However, when you add new code in your main body that requires your new feature, you’ll have to detect your particular type.

Putting a feature in a base class might mean that, for the benefit of one particular class, all the other classes derived from that base require some meaningless stub of a virtual function. This makes the interface less clear and annoys those who must redefine pure virtual functions when they derive from that base class. For example, suppose that in the Wind5.cpp program in Chapter XX you wanted to clear the spit valves of all the instruments in your orchestra that had them. One option is to put a virtual ClearSpitValve( ) function in the base class Instrument, but this is confusing because it implies that Percussion and electronic instruments also have spit valves. RTTI provides a much more reasonable solution in this case because you can place the function in the specific class (Wind in this case) where it’s appropriate.

Finally, RTTI will sometimes solve efficiency problems. If your code uses polymorphism in a nice way, but it turns out that one of your objects reacts to this general-purpose code in a horribly inefficient way, you can pick that type out using RTTI and write case-specific code to improve the efficiency.

Revisiting the trash recycler

Here’s the trash recycling simulation from Chapter XX, rewritten to use RTTI instead of building the information into the class hierarchy:

//: C08:Recycle2.cpp
// Chapter XX example w/ RTTI
#include "../purge.h"
#include <fstream>
#include <vector>
#include <typeinfo>
#include <cstdlib>
#include <ctime>
using namespace std;
ofstream out("recycle2.out");

class Trash {
  float _weight;
public:
  Trash(float wt) : _weight(wt) {}
  virtual float value() const = 0;
  float weight() const { return _weight; }
  virtual ~Trash() { out << "~Trash()\n"; }
};

class Aluminum : public Trash {
  static float val;
public:
  Aluminum(float wt) : Trash(wt) {}
  float value() const { return val; }
  static void value(int newval) {
    val = newval;
  }
};

float Aluminum::val = 1.67;

class Paper : public Trash {
  static float val;
public:
  Paper(float wt) : Trash(wt) {}
  float value() const { return val; }
  static void value(int newval) {
    val = newval;
  }
};

float Paper::val = 0.10;

class Glass : public Trash {
  static float val;
public:
  Glass(float wt) : Trash(wt) {}
  float value() const { return val; }
  static void value(int newval) {
    val = newval;
  }
};

float Glass::val = 0.23;

// Sums up the value of the Trash in a bin:
template<class Container> void
sumValue(Container& bin, ostream& os) {
  typename Container::iterator tally = 
    bin.begin();
  float val = 0;
  while(tally != bin.end()) {
    val += (*tally)->weight() * (*tally)->value();
    os << "weight of "
        << typeid(*tally).name()
        << " = " << (*tally)->weight() << endl;
    tally++;
  }
  os << "Total value = " << val << endl;
}

int main() {
  srand(time(0)); // Seed random number generator
  vector<Trash*> bin;
  // Fill up the Trash bin:
  for(int i = 0; i < 30; i++)
    switch(rand() % 3) {
      case 0 :
        bin.push_back(new Aluminum(rand() % 100));
        break;
      case 1 :
        bin.push_back(new Paper(rand() % 100));
        break;
      case 2 :
        bin.push_back(new Glass(rand() % 100));
        break;
    }
  // Note difference w/ chapter 14: Bins hold
  // exact type of object, not base type:
  vector<Glass*> glassBin;
  vector<Paper*> paperBin;
  vector<Aluminum*> alBin;
  vector<Trash*>::iterator sorter = bin.begin();
  // Sort the Trash:
  while(sorter != bin.end()) {
    Aluminum* ap =
      dynamic_cast<Aluminum*>(*sorter);
    Paper* pp =
      dynamic_cast<Paper*>(*sorter);
    Glass* gp =
      dynamic_cast<Glass*>(*sorter);
    if(ap) alBin.push_back(ap);
    if(pp) paperBin.push_back(pp);
    if(gp) glassBin.push_back(gp);
    sorter++;
  }
  sumValue(alBin, out);
  sumValue(paperBin, out);
  sumValue(glassBin, out);
  sumValue(bin, out);
  purge(bin);
} ///:~


The nature of this problem is that the trash is thrown unclassified into a single bin, so the specific type information is lost. But later, the specific type information must be recovered to properly sort the trash, and so RTTI is used. In Chapter XX, an RTTI system was inserted into the class hierarchy, but as you can see here, it’s more convenient to use C++’s built-in RTTI.

Mechanism & overhead of RTTI

Typically, RTTI is implemented by placing an additional pointer in the VTABLE. This pointer points to the typeinfo structure for that particular type. (Only one instance of the typeinfo structure is created for each new class.) So the effect of a typeid( ) expression is quite simple: The VPTR is used to fetch the typeinfo pointer, and a reference to the resulting typeinfo structure is produced. Also, this is a deterministic process – you always know how long it’s going to take.

For a dynamic_cast<destination*>(source_pointer), most cases are quite straightforward: source_pointer’s RTTI information is retrieved, and RTTI information for the type destination* is fetched. Then a library routine determines whether source_pointer’s type is of type destination* or a base class of destination*. The pointer it returns may be slightly adjusted because of multiple inheritance if the base type isn’t the first base of the derived class. The situation is (of course) more complicated with multiple inheritance where a base type may appear more than once in an inheritance hierarchy and where virtual base classes are used.

Because the library routine used for dynamic_cast must check through a list of base classes, the overhead for dynamic_cast is higher than typeid( ) (but of course you get different information, which may be essential to your solution), and it’s nondeterministic because it may take more time to discover a base class than a derived class. In addition, dynamic_cast allows you to compare any type to any other type; you aren’t restricted to comparing types within the same hierarchy. This adds extra overhead to the library routine used by dynamic_cast.

Creating your own RTTI

If your compiler doesn’t yet support RTTI, you can build it into your class libraries quite easily. This makes sense because RTTI was added to the language after observing that virtually all class libraries had some form of it anyway (and it was relatively “free” after exception handling was added because exceptions require exact knowledge of type information).

Essentially, RTTI requires only a virtual function to identify the exact type of the class, and a function to take a pointer to the base type and cast it down to the more derived type; this function must produce a pointer to the more derived type. (You may also wish to handle references.) There are a number of approaches to implement your own RTTI, but all require a unique identifier for each class and a virtual function to produce type information. The following uses a static member function called dynacast( ) that calls a type information function dynamic_type( ). Both functions must be defined for each new derivation:

//: C08:Selfrtti.cpp
// Your own RTTI system
#include "../purge.h"
#include <iostream>
#include <vector>
using namespace std;

class Security {
protected:
  static const int baseID = 1000;
public:
  virtual int dynamic_type(int id) {
    if(id == baseID) return 1;
    return 0;
  }
};

class Stock : public Security {
protected:
  static const int typeID = baseID + 1;
public:
  int dynamic_type(int id) {
    if(id == typeID) return 1;
    return Security::dynamic_type(id);
  }
  static Stock* dynacast(Security* s) {
    if(s->dynamic_type(typeID))
      return (Stock*)s;
    return 0;
  }
};

class Bond : public Security {
protected:
  static const int typeID = baseID + 2 ;
public:
  int dynamic_type(int id) {
    if(id == typeID) return 1;
    return Security::dynamic_type(id);
  }
  static Bond* dynacast(Security* s) {
    if(s->dynamic_type(typeID))
      return (Bond*)s;
    return 0;
  }
};

class Commodity : public Security {
protected:
  static const int typeID = baseID + 3;
public:
  int dynamic_type(int id) {
    if(id == typeID) return 1;
    return Security::dynamic_type(id);
  }
  static Commodity* dynacast(Security* s) {
    if(s->dynamic_type(typeID))
      return (Commodity*)s;
    return 0;
  }
  void special() {
    cout << "special Commodity function\n";
  }
};

class Metal : public Commodity {
protected:
  static const int typeID = baseID + 4;
public:
  int dynamic_type(int id) {
    if(id == typeID) return 1;
    return Commodity::dynamic_type(id);
  }
  static Metal* dynacast(Security* s) {
    if(s->dynamic_type(typeID))
      return (Metal*)s;
    return 0;
  }
};

int main() {
  vector<Security*> portfolio;
  portfolio.push_back(new Metal);
  portfolio.push_back(new Commodity);
  portfolio.push_back(new Bond);
  portfolio.push_back(new Stock);
  vector<Security*>::iterator it = 
    portfolio.begin();
  while(it != portfolio.end()) {
    Commodity* cm = Commodity::dynacast(*it);
    if(cm) cm->special();
    else cout << "not a Commodity" << endl;
    it++;
  }
  cout << "cast from intermediate pointer:\n";
  Security* sp = new Metal;
  Commodity* cp = Commodity::dynacast(sp);
  if(cp) cout << "it's a Commodity\n";
  Metal* mp = Metal::dynacast(sp);
  if(mp) cout << "it's a Metal too!\n";
  purge(portfolio);
} ///:~

Each subclass must create its own typeID, redefine the virtual dynamic_type( ) function to return that typeID, and define a static member called dynacast( ), which takes the base pointer (or a pointer at any level in a deeper hierarchy – in that case, the pointer is simply upcast).

In the classes derived from Security, you can see that each defines its own typeID enumeration by adding to baseID. It’s essential that baseID be directly accessible in the derived class because the enum must be evaluated at compile-time, so the usual approach of reading private data with an inline function would fail. This is a good example of the need for the protected mechanism.

The enum baseID establishes a base identifier for all types derived from Security. That way, if an identifier clash ever occurs, you can change all the identifiers by changing the base value. (However, because this scheme doesn’t compare different inheritance trees, an identifier clash is unlikely). In all the classes, the class identifier number is protected, so it’s directly available to derived classes but not to the end user.

This example illustrates what built-in RTTI must cope with. Not only must you be able to determine the exact type, you must also be able to find out whether your exact type is derived from the type you’re looking for. For example, Metal is derived from Commodity, which has a function called special( ), so if you have a Metal object you can call special( ) for it. If dynamic_type( ) told you only the exact type of the object, you could ask it if a Metal were a Commodity, and it would say “no,” which is untrue. Therefore, the system must be set up so it will properly cast to intermediate types in a hierarchy as well as exact types.

The dynacast( ) function determines the type information by calling the virtual dynamic_type( ) function for the Security pointer it’s passed. This function takes an argument of the typeID for the class you’re trying to cast to. It’s a virtual function, so the function body is the one for the exact type of the object. Each dynamic_type( ) function first checks to see if the identifier it was passed is an exact match for its own type. If that isn’t true, it must check to see if it matches a base type; this is accomplished by making a call to the base class dynamic_type( ). Just like a recursive function call, each dynamic_type( ) checks against its own identifier. If it doesn’t find a match, it returns the result of calling the base class dynamic_type( ). When the root of the hierarchy is reached, zero is returned to indicate no match was found.

If dynamic_type( ) returns one (for “true”) the object pointed to is either the exact type you’re asking about or derived from that type, and dynacast( ) takes the Security pointer and casts it to the desired type. If the return value is false, dynacast( ) returns zero to indicate the cast was unsuccessful. In this way it works just like the C++ dynamic_cast operator.

The C++ dynamic_cast operator does one more thing the above scheme can’t do: It compares types from one inheritance hierarchy to another, completely separate inheritance hierarchy. This adds generality to the system for those unusual cases where you want to compare across hierarchies, but it also adds some complexity and overhead.

You can easily imagine how to create a DYNAMIC_CAST macro that uses the above scheme and allows an easier transition to the built-in dynamic_cast operator.

Explicit cast syntax

Whenever you use a cast, you’re breaking the type system. [25] You’re telling the compiler that even though you know an object is a certain type, you’re going to pretend it is a different type. This is an inherently dangerous activity, and a clear source of errors.

Unfortunately, each cast is different: the name of the pretender type surrounded by parentheses. So if you are given a piece of code that isn’t working correctly and you know you want to examine all casts to see if they’re the source of the errors, how can you guarantee that you find all the casts? In a C program, you can’t. For one thing, the C compiler doesn’t always require a cast (it’s possible to assign dissimilar types through a void pointer without being forced to use a cast), and the casts all look different, so you can’t know if you’ve searched for every one.

To solve this problem, C++ provides a consistent casting syntax using four reserved words: dynamic_cast (the subject of the first part of this chapter), const_cast, static_cast, and reinterpret_cast. This window of opportunity opened up when the need for dynamic_cast arose – the meaning of the existing cast syntax was already far too overloaded to support any additional functionality.

By using these casts instead of the (newtype) syntax, you can easily search for all the casts in any program. To support existing code, most compilers have various levels of error/warning generation that can be turned on and off. But if you turn on full errors for the explicit cast syntax, you can be guaranteed that you’ll find all the places in your project where casts occur, which will make bug-hunting much easier.

The following table describes the different forms of casting:

static_cast

For “well-behaved” and “reasonably well-behaved” casts, including things you might now do without a cast (e.g., an upcast or automatic type conversion).

const_cast

To cast away const and/or volatile.

dynamic_cast

For type-safe downcasting (described earlier in the chapter).

reinterpret_cast

To cast to a completely different meaning. The key is that you’ll need to cast back to the original type to use it safely. The type you cast to is typically used only for bit twiddling or some other mysterious purpose. This is the most dangerous of all the casts.

The three explicit casts will be described more completely in the following sections.

Summary

RTTI is a convenient extra feature, a bit of icing on the cake. Although normally you upcast a pointer to a base class and then use the generic interface of that base class (via virtual functions), occasionally you get into a corner where things can be more effective if you know the exact type of the object pointed to by the base pointer, and that’s what RTTI provides. Because some form of virtual-function-based RTTI has appeared in almost all class libraries, this is a useful feature because it means

  1. You don’t have to build it into your own libraries.
  2. You don’t have to worry whether it will be built into someone else’s library.
  3. You don’t have the extra programming overhead of maintaining an RTTI scheme during inheritance.
  4. The syntax is consistent, so you don’t have to figure out a new one for each library.

While RTTI is a convenience, like most features in C++ it can be misused by either a naive or determined programmer. The most common misuse may come from the programmer who doesn’t understand virtual functions and uses RTTI to do type-check coding instead. The philosophy of C++ seems to be to provide you with powerful tools and guard for type violations and integrity, but if you want to deliberately misuse or get around a language feature, there’s nothing to stop you. Sometimes a slight burn is the fastest way to gain experience.

The explicit cast syntax will be a big help during debugging because casting opens a hole into your type system and allows errors to slip in. The explicit cast syntax will allow you to more easily locate these error entryways.

Exercises

  1. Modify C16:AutoCounter.h in volume 1 of this book so that it becomes a useful debugging tool. It will be used as a nested member of each class that you are interested in tracing. Turn AutoCounter into a template that takes the class name of the surrounding class as the template argument, and in all the error messages use RTTI to print out the name of the class.
  2. Use RTTI to assist in program debugging by printing out the exact name of a template using typeid( ). Instantiate the template for various types and see what the results are.
  3. Implement the function TurnColorIfYouAreA( ) described earlier in this chapter using RTTI.
  4. Modify the Instrument hierarchy from Chapter XX by first copying Wind5.cpp to a new location. Now add a virtual ClearSpitValve( ) function to the Wind class, and redefine it for all the classes inherited from Wind. Instantiate a TStash to hold Instrument pointers and fill it up with various types of Instrument objects created using new. Now use RTTI to move through the container looking for objects in class Wind, or derived from Wind. Call the ClearSpitValve( ) function for these objects. Notice that it would unpleasantly confuse the Instrument base class if it contained a ClearSpitValve( ) function.

[25] See Josée Lajoie , “The new cast notation and the bool data type,” C++ Report, September, 1994 pp. 46-51.

[ Previous Chapter ] [ Short TOC ] [ Table of Contents ] [ Index ] [ Next Chapter ]
Last Update:02/22/2000