RTTI Fundamentals

Run-Time Type Information in C++

C++ offers some minimal dynamic type inspection under the umbrella term “RTTI”. Most pro­grams do without, and several compilers either turn it off by default, or have a compile time op­tion to en/disable it. There are not many facilities, and they are briefly presented here.

PREREQUISITES — You should already…
  • have non-trivial C++ language & programming background.
  • be experienced in creating and compiling C++ programs.
  • understand the terms runtime, compile-time, dynamic, static & polymorphic in a C++ context.

Introduction

RTTI is a dynamic mechanism, which means it operates during runtime, and cannot gen­e­ral­ly re­port er­rors at com­pile-time. Dynamic type conversion will therefore return nullptr or throw an exception during runtime. This is the only reflection support you will find in C++.

NOTECompile-Time Conversions

Type conversions, including dynamic conversions, are all evaluated for val­id­ity at com­pile-time, within reason. If the com­pil­er cannot detect an obvious problem, the cast may be al­low­ed, but the result could be in­valid; using it in such a case will generally result in a crash, or invalid memory access.

RTTI is not a general reflection mechanism — it only works up and down an inheritance hierarchy. And even then, it only works if the classes involved are polymorphic (implement or inherit at least one virtual function).

RTTI Operators

There are two operators that use RTTI: typeid(), and dynamic_cast<>(expr). They are both brief­ly dis­cus­sed below.

Type Information

The typeid(expr›‖‹type) operator provides information about a type. This operator returns a std::type_info ob­ject, declared in the <typeinfo> header, which must be included before using typeid(), other­wise the program is ill-formed.

For polymorphic types, the in­for­ma­tion can be retrieved at runtime, and for other types, the in­for­ma­tion is determined at compile-time, if possible. Type qualifiers, like const or volatile, are ig­nored by typeid().

A simply way to use typeid, is to retrieve the name of the type via the name() member function:

   std::cout << typeid(double).name() << std::endl;

The output from GCC will be mangled, but can be filtered (piped) through the GCC command line util­i­ty c++filt, with the -t switch, to obtain more readable names. Alternatively, several util­i­ty func­tions using the GCC C++ ABI can be found to demangle names in the program itself.

It can be used to check if ‹expr1› and ‹expr2› (for example) have the same type:

   if (typeid(‹expr1›).hash_code() == typeid(‹expr2›).hash_code())...

Designs should not be built around this ability — it is mostly a mechanism used as a last resort.

Dynamic Cast

The dynamic_cast operator works only with polymorphic types (classes with one or more vir­tual func­tions, either defined in the respective classes, or inherited from base classes). Used on pointer conversions, it will return nullptr if the conversion fails. Using dynamic_cast for casting to a ref­er­en­ce type, may cause it to throw a std::bad_cast exception.

Although dynamic_cast can be used for upcasts, it is not necessary, since conversion to a base class pointer is always implicit.

rttidemo.cppMinimal RTTI Example
/*!@file  rttidemo.cpp
*  @brief Minimalistic RTTI Demonstration
*/
#include <iostream>
using std::cout; using std::endl;

class PolyBase { // class with (at least) one virtual function
   public: virtual ~PolyBase() { }
   };

class PolyDeriv : public PolyBase { // inherits a virtual function 
   //...
   };

int main () {

   PolyBase  base_obj{};
   PolyDeriv deriv_obj{};
   PolyBase* base_ptr{nullptr};

   // use `typeid()` to determine if `base_ptr` points to a `PolyDeriv`
   // object.  we could also have compared `type_info` objects directly.
   //
   base_ptr = &deriv_obj;
   if (typeid(*base_ptr).hash_code() == typeid(PolyDeriv).hash_code())
      cout << "base_ptr points to a PolyDeriv object" << endl;
   else
      cout << "base_ptr does not point to a PolyDeriv object" << endl;

   // use `dynamic_cast` for downcast from `PolyBase*` to `PolyDeriv*`.
   //
   if (dynamic_cast<PolyDeriv*>(base_ptr))
      cout << "base_ptr points to a PolyDeriv object" << endl;
   else
      cout << "base_ptr does not point to a PolyDeriv object" << endl;

   // same code, but with `base_ptr` pointing to a `PolyBase` object:
   //
   base_ptr = &base_obj;
   if (dynamic_cast<PolyDeriv*>(base_ptr))
      cout << "base_ptr points to a PolyDeriv object" << endl;
   else
      cout << "base_ptr does not point to a PolyDeriv object" << endl;

   return EXIT_SUCCESS;
   }

Of course, static_cast could have been used, but it would be a compile-time cast, and will not report a failure — the program may simply crash if you used a PolyBase* as a PolyDeriv* if it does not actually point to a PolyDeriv object. At least with dynamic_cast, a nullptr is returned on failure.

WARNINGCareful with Casts

Calling a derived class function on an incorrectly converted base class pointer may in fact work, but the derived data will appear corrupt, since it does not exist — just like indexing past the end of a C-style array, which may corrupt data or crash, or not.

It is really as simple as that. And no more, as far as C++ reflection goes, but do not forget about static type checking via facilities in the <type_traits> header.


2017-11-18: Update to new admonitions. [brx]
2017-09-20: Created. Edited. [brx;jjc]