typeid()
typeid()
operatorIn 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.
typeid()
operatorThe 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
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.
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.
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 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.
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
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
.
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
This lesson explored Run-Time Type Information (RTTI) and features it supports. The key takeaways include:
typeid()
operator to obtain type information of expressions and types.==
and !=
operators with std::type_info
.typeid()
in working with polymorphic types and its comparison with dynamic_cast()
.typeid()
with template types for runtime type identification.std::type_info
and the introduction of std::type_index
for more flexible type handling.typeid()
Learn to identify and react to object types at runtime using RTTI, dynamic casting and the typeid()
operator
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.