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
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: intWe 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: intLater 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: floatComparing 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 floatThe 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 dragonWe 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 dragonThe 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 dragonUsing 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 functionstd::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 dragonRather 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 ThingsSummary
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 withstd::type_info. - Application of
typeid()in working with polymorphic types and its comparison withdynamic_cast(). - Use of
typeid()with template types for runtime type identification. - Understanding the limitations of
std::type_infoand the introduction ofstd::type_indexfor more flexible type handling. - Strategies for type deduction without relying on RTTI, illustrating alternative approaches in resource-constrained environments.
Errors and Assertions
Learn how we can ensure that our application is in a valid state using compile-time and run-time assertions.