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.
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.
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.
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:
//: C09:Rtshapes.cpp // Counting shapes //{L} ../TestSuite/Test #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.
For consistency, the
typeid( )
operator works with built-in types. So the following expressions are
true:
//: C09:TypeidAndBuiltins.cpp //{L} ../TestSuite/Test #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*)); } ///:~
typeid( ) must work properly
in all situations. For example, the following class contains a nested
class:
//: C09:RTTIandNesting.cpp //{L} ../TestSuite/Test #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.
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,
//: C09:RTTIWithoutPolymorphism.cpp //{L} ../TestSuite/Test #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)
dynamic_cast can detect both exact
types and, in an inheritance hierarchy with multiple levels, intermediate types.
For example,
//: C09:DynamicCast.cpp // Using the standard dynamic_cast operation //{L} ../TestSuite/Test #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*)
//: C09:Voidrtti.cpp // RTTI & void pointers //{L} ../TestSuite/Test #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; } ///:~
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:
//: C09:ConstructorOrder.cpp // Order of constructor calls //{L} ../TestSuite/Test #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.
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:
//: C09:RTTIwithReferences.cpp //{L} ../TestSuite/Test #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)
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:
//: C09:RTTIwithExceptions.cpp //{L} ../TestSuite/Test #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.
Of course, the RTTI mechanisms must work
properly with all the complexities of multiple inheritance, including
virtual base classes:
//: C09:RTTIandMultipleInheritance.cpp //{L} ../TestSuite/Test #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
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.
Here’s the trash recycling
simulation from Chapter XX, rewritten to use RTTI instead of building the
information into the class hierarchy:
//: C09:Recycle2.cpp // Chapter XX example w/ RTTI //{L} ../TestSuite/Test #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.
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.
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:
//: C09:Selfrtti.cpp // Your own RTTI system //{L} ../TestSuite/Test #include "../purge.h" #include <iostream> #include <vector> using namespace std; class Security { protected: enum { baseID = 1000 }; public: virtual int dynamic_type(int id) { if(id == baseID) return 1; return 0; } }; class Stock : public Security { protected: enum { 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: enum { 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: enum { 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: enum { 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.
Whenever you use a cast, you’re
breaking the type system.
[23]
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.
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. |
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
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.
[23]
See Josée Lajoie , “The new cast notation and the bool data
type,” C++ Report, September, 1994 pp. 46-51.