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:
//: 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.
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*)); } ///:~
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.
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)
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*)
//: 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; } ///:~
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.
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)
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.
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
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:
//: 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.
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:
//: 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.
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.
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.
[25] See
Josée Lajoie , “The new cast notation and the bool data
type,” C++ Report, September, 1994 pp. 46-51.