dcsimg

What Is Runtime Type Identification (RTTI) in C++?

Overview

RTTI stands for Runtime type identification. It is a mechanism to find the type of an object dynamically from an available pointer or reference to the base type. This is particularly useful in a situation where we do not want to rely on the type identification by the virtual function mechanism. Most class libraries contain virtual functions to produce runtime type information. This feature was added to C++ with the introduction to exception handling because knowing the runtime type was vital in dealing with exceptions. The RTTI provides an explicit way to identify the runtime type separately from what is possible with the virtual function mechanism.

Runtime Casts

The simplest way to determine the runtime type of an object using a pointer or reference is to use the runtime cast that verifies that the cast is valid. This is particularly useful when we want to cast a base class pointer to its derived type. This is shown in Figure 1.

Determining the runtime type of an object
Figure 1: Determining the runtime type of an object

The casting of an object is mainly required when dealing with the inheritance hierarchy of classes where we may down cast an object reference or pointer. So, primarily there are two types of casting: upcasting and down casting.

Upcasting

Upcasting is the process where we treat a pointer or a reference of a derived class object as a base class pointer. Unlike down casting, upcasting of a pointer is automatically accomplished by assigning a derived class pointer or a reference to its base class pointer. This can be accomplished simply as follows:

Shape *shape_ptr = nullptr;
Rectangle rec(10.0,20.0);
shape_ptr = &rec;

Down Casting

Down casting is converting a base class pointer or reference to a derived class pointer. This not as simple as upcasting for various reasons. But, the following code snippet illustrates what we mean.

Shape *shape_ptr = nullptr;
Rectangle rec_ref(10.0,20.0);
shape_ptr = &rec_ref;
Rectangle *rec_ptr = nullptr;
rec_ptr = (Rectangle *)shape_ptr;

Note that down casting requires explicit casts, such as (Rectangle *) shape_ptr. The code given above may compile and run without any problem, but understand that this is not a very good idea to cast an base class pointer to a derived type in this manner. Intuitively, one simple reason for this is that derived classes are an extension of the base class and usually contain more information than the base class. This can result in an unexpected loss of information during casting. Therefore, we must ensure that no such loss of information occurs or necessary error flag is raised if the casting cannot be done in a proper manner. This is where we need the mechanism of dynamic cast.

Using dynamic_cast

We can use dynamic_cast for safe down casting of a base class pointer or a reference to a subclass in an inheritance hierarchy. On successful casting, it returns a pointer of the converted type and, if we try to cast a invalid type such as a object pointer which is not of the type of the desired subclass, it fails but does it without creating a major problem.

The syntax of dynamic_cast with pointer and reference respectively are:

<type> *ptr_derived = dynamic_cast<<type> *>(ptr_obj);

<type> ref_derived = dynamic_cast<<type> &> (ref_obj);

Because it is not possible to return nullptr as an indication of an error when casting a reference, dynamic_cast throws an exception as defined in the typeinfo header called std::bad_cast. Therefore, it is a good programming practice to wrap dynamic cast operations within a try/catch block.

Shape *shape_ptr = nullptr;
Rectangle rec_ref(10.0,20.0);
shape_ptr = &rec_ref;
Rectangle *rec_ptr = nullptr;
// rec_ptr = (Rectangle *)shape_ptr;
try {
   rec_ptr = dynamic_cast<Rectangle *>(shape_ptr);
} catch (std::bad_cast &bc) {
   cerr<<bc.what()<<endl;
}

A Quick Example

The following example is a small, elaborate illustration of the runtime casting with dynamic cast. The output is shown in Figure 2.

#include <iostream>
#include <vector>
#include <cmath>
#include <typeinfo>

using namespace std;

class Shape {
protected:
   std::string name;
   double area;
   double perimeter;

public:
   Shape(){name = ""; area = 0.0; perimeter=0.0;}
   virtual ~Shape();
   virtual std::string toString() const {
      return "Name: "+name+
            "\nArea "+to_string(area)+
            "\nPerimeter "+to_string(perimeter);
   }
};

class Circle: public Shape{
   double radius;
public:
   Circle(double r){
      radius = r;
      Shape::name = "Circle";
      Shape::area = M_PI * pow(radius, 2);
      Shape::perimeter = 2 * M_PI * radius;
   }
   std::string toString() const{
      return Shape::toString() +
            "\nRadius = "+std::to_string(radius);
   }
};

class Rectangle: public Shape {
   double length, width;
public:
   Rectangle(double l, double w) {
      length = l;
      width = w;
      Shape::name="Rectangle";
      Shape::area = length * width;
      Shape::perimeter = 2 * (length + width);
   }
   std::string toString() const{
      return Shape::toString() +
            "\nLength = "+std::to_string(length)+
            "\nWidth = "+std::to_string(width);
   }
};

template<class T> void cleanup(T &t){
   typename T::iterator i;
   for (i=t.begin(); i != t.end(); ++i) {
      delete *i;
      *i = nullptr;
   }
}

int main()
{

   vector<Shape *> pf;
   pf.push_back(new Circle(5.0));
   pf.push_back(new Rectangle(6.2,5.6));
   pf.push_back(new Rectangle(3.5,2.5));
   pf.push_back(new Circle(8.9));

   try {
      for(vector<Shape *>::iterator iter = pf.begin();
            iter != pf.end(); ++iter){
            Rectangle *rptr =
                  dynamic_cast<Rectangle *>(*iter);
         if(rptr)
            cout<<rptr->toString()<<endl;
         else
            cout<<"Not a rectangle"<<endl;
      }
   } catch (std::bad_cast &bc) {
      cerr<<bc.what()<<endl;
   }

   try {
      Shape *sptr = new Circle(3.6);
      Rectangle *rptr = dynamic_cast<Rectangle *>(sptr);
      if(rptr)
         cout<<"Rectangle object"<<endl;
      Circle *cptr = dynamic_cast<Circle *>(sptr);
      if(cptr)
         cout<<"It is also a circle object"<<endl;
   } catch (std::bad_cast &bc) {
      cerr<<bc.what()<<endl;
   }

   cleanup(pf);
   return 0;
}

Output

The output from "A Quick Example"
Figure 2: The output from "A Quick Example"

Using the typeid Operator

There is an operator, called typeid, which can be used to get the runtime information of an object. This operator returns a type_info instance. The std::type_info is a class that holds implementation-specific information of a type, such as type name, and means to compare the equality of two types. If the type is polymorphic, the typeid operator provides the information about the most derived type that applies to the dynamic type; otherwise, it provides static type information. We can use the typeid operator to get the name of the dynamic type object as a constant character pointer. You can see the output in Figure 3.

#include <iostream>
#include <typeinfo>

using namespace std;

class Base_A {
public:
   virtual ~Base_A(){}
};

class Derived_A: public Base_A{
public:
   Derived_A(){}
};

class Base_B{

};

class Derived_B: public Base_B{
public:
   Derived_B(){}
};

int main()
{
   const Derived_A ref_da;
   const Base_A *pa = &ref_da;
   cout<<typeid (pa).name()<<endl;

   cout<<typeid (*pa).name()<<endl;
   cout<<(typeid (*pa)==typeid (ref_da)
         ? "true":"false")<<endl;
   cout <<(typeid(Derived_A) == typeid(const Derived_A)
         ? "true":"false")<< endl;

   const Derived_B ref_db;
   const Base_B *pb = &ref_db;

   cout<<typeid (pb).name()<<endl;
   cout<<typeid (*pb).name()<<endl;
   cout<<(typeid (*pb)==typeid (ref_db)
         ? "true":"false")<<endl;
   cout <<(typeid(Derived_B) == typeid(const Derived_B)
         ? "true":"false")<< endl;

   return 0;
}

Output

Output from the typeid operator code sample
Figure 3: Output from the typeid operator code sample

Runtime Casts with Multiple Inheritance

Dynamic casts with single inheritance are fine, but what about multiple inheritance? Can dynamic_cast detect exact types in a complex scenario of multiple inheritance hierarchical level? The answer is yes, it does. Here is an example scenario.

#include <iostream>
#include <typeinfo>
#include <cassert>
using namespace std;

class Father{
public:
   virtual  ~Father(){}
};
class Mother{
public:
   virtual  ~Mother(){}
};
class Child: public Father, public Mother{};
class GrandChild: public Child{};

int main()
{
   Mother *ptr_mother = new GrandChild;
   GrandChild *ptr_grandchild = dynamic_cast<GrandChild
      *>(ptr_mother);
   Child *ptr_dab = dynamic_cast<Child*>(ptr_mother);
   Father *ptr_father = dynamic_cast<Father *>(ptr_mother);
   assert(typeid(ptr_mother) != typeid (GrandChild*));
   assert(typeid (ptr_mother) == typeid (Mother*));
   delete ptr_mother;

   return 0;
}

Conclusion

The RTTI also works with template classes and there is absolutely no reason not to do that. After all, they also generate classes. The RTTI, therefore, provides a convenient way to get the name of the class we currently are working on. Understand that the mechanism of RTTI should be used only when necessary, because the most of such need is fulfilled by the virtual functions. In fact, RTTI functions were more frequently used when virtual the function mechanism was not implemented in C++ classes. The idea is that RTTI should be used only when the polymorphism in code is unable to serve the purpose. The use of RTTI may leverages efficiency at times, but defeats the purpose of polymorphic niceties. This may create a problem in code maintenance in the long run.



This article was originally published on March 11th, 2020

About the Author

Manoj Debnath

manosolireap@outlook.com

Related Articles

Most Popular Programming Stories

More for Developers

RSS Feeds

Thanks for your registration, follow us on our social networks to keep up-to-date