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

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:

//: C24: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.

Contents | Prev | Next


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