Run Time Type Information (RTTI) and typeid()

Learn to identify and react to object types at runtime using RTTI, dynamic casting and the typeid() operator

Ryan McCombe
Updated

In our introductory course, we introduced the concepts of inheritance and polymorphism. One of the implications of these techniques is that, when one of our functions receive a reference to an object, they don't necessarily know the exact type of the object.

In the following example, our Handle() function knows on each invocation it is going to be receiving a reference to a Monster, but it doesn't know the exact type of that Monster. It could be a basic Monster, or it could be any subtype of Monster, such as a Dragon:

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

class Dragon : public Monster {};

void Handle(Monster& Enemy) {}

To help us out with scenarios like this, compilers add some additional information to our compiled programs, enabling us to write code that can figure out what type any object is at run time.

This mechanism is called Run-Time Type Information (RTTI), and it unlocks two features that can help us - the typeid() operator, and dynamic casting.

The typeid() Operator

The main way we can get information about a type at run time is through the typeid() operator, available after including <typeinfo>:

#include <typeinfo>

int main() {
  int Number{42};
  typeid(Number);
}

This returns a std::type_info object, which we can capture by constant reference:

#include <typeinfo>

int main() {
  int Number{42};
  const std::type_info& TypeInfo{typeid(Number)};
}

The std::type_info type has a name() method, which returns a string representation of what the type is. This method is rarely used within the logic of our program, but it can be helpful for logging and debugging:

#include <typeinfo>
#include <iostream>

int main() {
  int Number{42};
  const std::type_info& TypeInfo{typeid(Number)};

  std::cout << "Number has type: "
    << TypeInfo.name();
}
Number has type: int

We can also directly provide a type to the typeid() operator, rather than an expression. This somewhat useless example demonstrates this in action:

#include <iostream>
#include <typeinfo>

int main() {
  std::cout << "int has type: "
    << typeid(int).name();
}
int has type: int

Later in this lesson, we'll see the main use cases where passing a type to typeid is practically useful. However, one situation where we may find it helpful is when the type name is a template parameter:

#include <iostream>
#include <typeinfo>

template <typename T>
struct Container {
  void Log() {
    std::cout << "\nI am storing type: "
      << typeid(T).name();
  }

  T Value;
};

int main() {
  Container<int> IntContainer;
  IntContainer.Log();

  Container<float> FloatContainer;
  FloatContainer.Log();
}
I am storing type: int
I am storing type: float

Comparing Types at Runtime

The most practically useful feature of the std::type_info type is that it has implemented the == and != operators. This allows us to compare types within our program, and implement different runtime behavior as needed:

#include <iostream>
#include <typeinfo>

int main() {
  int Number{42};

  if (typeid(Number) == typeid(int)) {
    std::cout << "Number is an int";
  }

  if (typeid(Number) != typeid(float)) {
    std::cout << "\nIt is not a float";
  }
}
Number is an int
It is not a float

The main use case for this is when we receive a pointer or reference to an object of a polymorphic type, and we need to find out whether it has a more derived subtype than what is represented by the pointer or reference type. We cover this in the next section.

Comparing Polymorphic Types

The main use case for typeid() comparisons is when working with polymorphic types, as an alternative to dynamic_cast(). We covered polymorphic types and dynamic casting in detail in our lesson on downcasting, and familiarity with those concepts will be very helpful for understanding the rest of this section:

Downcasting

Get to grips with downcasting in C++, including static and dynamic casts. This lesson provides clear explanations and practical examples for beginners

In that lesson, we introduced a scenario where we had a function that accepted a Monster by reference:

class Monster {/*...*/}; class Dragon : public Monster {}; void Log(Monster& Enemy) {} int main() { Dragon SomeDragon; Log(SomeDragon); Monster SomeGoblin; Log(SomeGoblin); }

Monster is a polymorphic type so, for any given invocation, the Log() function doesn't know the exact type of Enemy it is working with. It could be a basic Monster, or it could be any subclass of Monster, such as a Dragon.

We saw how dynamic_cast() could help our function figure out what it's dealing with, and react accordingly:

#include <iostream>

class Monster {/*...*/}; class Dragon : public Monster {}; void Log(Monster& Enemy) { Dragon* DragonPtr{ dynamic_cast<Dragon*>(&Enemy)}; if (DragonPtr) { std::cout << "That's a dragon\n"; } else { std::cout << "That's not a dragon\n"; } }
int main() {/*...*/}
That's a dragon
That's not a dragon

We can now implement this behavior more directly, using typeid() comparisons:

#include <iostream>
#include <typeinfo>

class Monster {/*...*/}; class Dragon : public Monster {}; void Log(Monster& Enemy) { if (typeid(Enemy) == typeid(Dragon)) { std::cout << "That's a dragon\n"; } else { std::cout << "That's not a dragon\n"; } }
int main() {/*...*/}
That's a dragon
That's not a dragon

The main advantage of dynamic_cast() over typeid() comparison is that it returns a pointer, which is useful for performing follow-up operations if the object indeed has the type we're testing for.

For example, if we wanted to access any Dragon class members, we'd still want to use dynamic_cast() here.

But if we don't need that, and we just need to find out what type we're dealing with, the typeid() approach is simpler, easier to understand, and slightly better for performance.

Comparing Template Types

Template parameters can also be polymorphic types, so it is sometimes useful to perform runtime comparisons on them using the typeid() operator. Below, we've replicated the previous example, except we've replaced our Monster type with a templated typename T.

Our template is instantiated once, to create a function that receives a Monster&, and then uses typeid() to determine which specific type of Monster it received at run time:

#include <iostream>
#include <typeinfo>

class Monster {/*...*/}; class Dragon : public Monster {}; template <typename T> void Log(T& Enemy) { if (typeid(Enemy) == typeid(Dragon)) { std::cout << "That's a dragon\n"; } else { std::cout << "That's not a dragon\n"; } } int main() { Dragon SomeDragon; Log<Monster>(SomeDragon); Monster SomeGoblin; Log<Monster>(SomeGoblin); }
That's a dragon
That's not a dragon

Using std::type_info and std::type_index

The std::type_info objects returned by the typeid() operator are closely linked to our compiler's internal workings, and its implementation of RTTI. As such, we're quite limited in what we can do with them. For example, we can't create copies of these objects:

#include <typeinfo>

int main() {
  int Number{42};
  std::type_info TypeInfo{typeid(Number)};
}
error C2280: 'type_info::type_info(const type_info &)': attempting to reference a deleted function

std::type_info objects not being copy-constructible is the reason we've been capturing them by const reference in previous examples.

To give us more flexibility when working with these objects, the C++ specification introduced the std::type_index type, which is designed to be a friendlier wrapper around a std::type_info object.

A std::type_index is constructible from a std::type_info, meaning it is constructible from what is returned by the typeid() operator:

#include <typeinfo>
#include <typeindex> 

int main() {
  int Number{42};
  std::type_index TypeInfo{typeid(Number)}; 
}

For more elaborate projects that require type info to be stored in variables and passed around to other functions, we'll often prefer to use std::type_index over the more primitive std::type_info.

Run Time Type Deduction without RTTI

To support RTTI, compilers need to insert additional data into our compiled binary, describing our types. This makes our compiled package larger so, as an optional optimization, compilers provide the option to disable RTTI entirely.

This can be useful in environments where resources are highly constrained, or for projects that don't require RTTI-based features (typeid and dynamic_cast)

When working in environments without RTTI, we can always recreate those capabilities in other ways. For example, we can simply add type information as members of classes that need it.

Below, our base Monster polymorphic type implements a GetType() method, which derived classes can override. This allows us to recreate the previous example, without needing typeid():

#include <iostream>

enum class MonsterType {Monster, Dragon};

class Monster {
 public:
   virtual MonsterType GetType() const {
     return MonsterType::Monster;
  }
};

class Dragon : public Monster {
 public:
   MonsterType GetType() const override {
     return MonsterType::Dragon;
  }
};

void Log(Monster& Enemy) {
   if (Enemy.GetType() == MonsterType::Dragon) {
     std::cout << "That's a dragon";
   }
}

int main() {
  Dragon SomeEnemy;
  Log(SomeEnemy);
}
That's a dragon

Rather than using dynamic_cast() to access derived class members, we can first examine the type to ensure the cast will succeed, and then use static_cast().

Below, we use static_cast() to generate a Dragon pointer, within an if statement that has already confirmed our Enemy is indeed a Dragon:

#include <iostream>

enum class MonsterType {Monster, Dragon};

class Monster {/*...*/}; class Dragon : public Monster { public: MonsterType GetType() const override { return MonsterType::Dragon; } void DragonFunction() { std::cout << "Dragon Things"; } }; void Log(Monster& Enemy) { if (Enemy.GetType() == MonsterType::Dragon) { Dragon* DragonPtr{ static_cast<Dragon*>(&Enemy)}; DragonPtr->DragonFunction(); } } int main() { Dragon SomeEnemy; Log(SomeEnemy); }
Dragon Things

Summary

This lesson explored Run-Time Type Information (RTTI) and features it supports. The key takeaways include:

  • Introduction to RTTI and its importance for identifying object types at runtime.
  • Usage of the typeid() operator to obtain type information of expressions and types.
  • How to compare types at runtime using the == and != operators with std::type_info.
  • Application of typeid() in working with polymorphic types and its comparison with dynamic_cast().
  • Use of typeid() with template types for runtime type identification.
  • Understanding the limitations of std::type_info and the introduction of std::type_index for more flexible type handling.
  • Strategies for type deduction without relying on RTTI, illustrating alternative approaches in resource-constrained environments.
Next Lesson
Lesson 35 of 128

Errors and Assertions

Learn how we can ensure that our application is in a valid state using compile-time and run-time assertions.

Questions & Answers

Answers are generated by AI models and may not have been reviewed. Be mindful when running any code on your device.

Using RTTI for a Plugin System
How can I use RTTI to implement a plugin system where different types of plugins are loaded dynamically?
Performance Impact of RTTI
What are the performance implications of using RTTI in a large-scale application?
RTTI for Generic Serialization
How can I use typeid() to implement a generic serialization system for complex object hierarchies?
RTTI with Abstract Base Classes
Is it possible to use RTTI with abstract base classes? If so, how?
Combining RTTI with Visitor Pattern
How can I combine RTTI with design patterns like Visitor to create more flexible architectures?
RTTI in Game Entity Systems
What are the best practices for using RTTI in game development, particularly for entity systems?
Type-Safe Event System with std::type_index
How can I use std::type_index to implement a type-safe event system?
RTTI and Application Security
Are there any security implications of using RTTI in applications that process untrusted data?
RTTI in Factory Pattern Implementation
How can I use RTTI to implement a factory pattern that creates objects based on runtime type information?
RTTI in Logging and Debugging
How can I use RTTI to implement a robust logging system that provides detailed type information for debugging?
RTTI in Cross-Platform Development
Are there any best practices for using RTTI in cross-platform development to ensure consistent behavior?
Or Ask your Own Question
Get an immediate answer to your specific question using our AI assistant