Run Time Type Information (RTTI) and typeid()

Learn to identify and react to object types at runtime in using RTTI, dynamic casting and the typeid() operator
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Illustration representing computer hardware
Ryan McCombe
Ryan McCombe
Posted

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

Investigating Types at Compile Time vs Run Time

Given the name() method is primarily used for debugging and logging, the previous example is reasonable. However, templates are instantiated at compile time, so it’s generally preferred to do type investigation at compile time where possible.

Features we covered earlier in the chapter, such as type traits, concepts and if constexpr are more powerful than what we can achieve via typeid(), and they don’t have the runtime performance cost.

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:

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 dynamic_cast() has over typeid() comparison is that it returns a pointer, which is useful if we need to perform any follow-up operations if the object did indeed have the type we were 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

Comparing Non-Polymorphic Template Types

The typeid() technique in the previous example will work if our type is not polymorphic, but it’s generally not the preferred approach.

If our type is known at compile time - and in most use cases, it is - we should investigate and react to it at compile time. This involves using compile time techniques we covered in previous lessons, such as if constexpr, type traits, and concepts.

The following code implements behavior similar to the previous example using these techniques. The if constexpr statement and std::same_as() concept change the code within the function the template generates at compile time:

#include <iostream>
#include <concepts>

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

If the template was instantiated by passing Dragon to the template argument T, then the if constexpr statement will cause a function to be generated that looks something like this:

void Log(Dragon Enemy) {
  std::cout << "That's a dragon\n";
}

If T has any other type, then we can imagine the generated function would look something like this:

void Log(SomeOtherType Enemy) {
  std::cout << "That's not a dragon\n";
}

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 importane 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.

Was this lesson useful?

Next Lesson

Errors and Assertions

Learn how we can ensure that our application is in a valid state using compile-time and run-time assertions.
3D Character Concept Art
Ryan McCombe
Ryan McCombe
Posted
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Type Traits and Concepts
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access

This course includes:

  • 124 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Errors and Assertions

Learn how we can ensure that our application is in a valid state using compile-time and run-time assertions.
3D Character Concept Art
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved