Notes on std:any and RTTI in C++ by talbi
2025-05-31
·
6 min read
- c++
- programming
Run time type information (RTTI) is a mechanism in programming languages which allows type
introspection at runtime. In C++, this is achieved using the typeid
operator:
struct Base {
virtual ~Base() = default;
};
struct Derived1 : Base {};
struct Derived2 : Base {};
auto main() -> int {
std::unique_ptr<Base> p = std::make_unique<Derived1>();
std::println("type of p = {}", typeid(*p).name());
}
This is neat at best, but not very useful outside some debugging. More useful, is that the object
returned by typeid
, is of type std::type_info
, and it also has an operator==
implementation.
The usage of dynamic_cast<>
requires RTTI as its implementation is quite straightforward:
template<typename T, typename U>
auto my_dynamic_cast(U* ptr) -> T* {
if (typeid(*ptr) != typeid(U))
return nullptr;
return reinterpret_cast<U*>(ptr);
}
With one caveat (two if you care about template fluff like std::remove_pointer_t
here and
std::add_pointer_t
there) that dynamic_cast<>
allows for “non-strict” casting too,
in the sense that this code won’t work with the above implementation:
struct Derived3 : Derived1 {};
auto main() -> int {
auto p = get_object();
if (auto d = dynamic_cast<Derived1*>(p.get())) {
}
}
In order to actually implement this we’d need std::type_info
to expose some kind of ordering
mechanism which respects base and derived classes.
There is std::type_info::before
which almost provides this functionality, however:
- The ordering is implementation defined and can change between program invocations
- Is too generic, for example
typeid(void).before(typeid(Derived1))
returnstrue
which might
make one think thatvoid
is a base class ofDerived1
(it isn’t).
Regardless, the implementation of dynamic_cast<>
just requires the compiler to save a DAG in the
binary and iterate on it whenever needed, which is to say this task isn’t impossible.
Moving on; The usage of dynamic_cast<>
or typeid
usually indicates a bad design, so you should
aim to avoid using it when possible.
C++ RTTI details
The benefits of RTTI don’t come for free, for there is some runtime cost in the sense that the
binary is larger, and it occupies more memory.
First, note that the compiler generates a std::type_info
structure only for the types which could
have typeid
called on them at runtime. This includes:
- Objects accessed in a non-polymorphic way for which we called
typeid
on. For example:
struct MyType { };
const auto& type_info = typeid(MyType);
- Objects with polymorphic types which we might have called
typeid
on. For example:
std::unique_ptr<Base> p = get_object();
const auto& type_info = typeid(*p);
To be exact, the std::type_info
is generated for each type which is instantiated somewhere in the
code. Which specifically just corresponds to whether the compiler has generated a virtual table
(vtable) for the type. No vtable means no pointer to the type information, so no need to generate
one.
So, how is typeid
implemented?
There’s no magic. The const std::type_info&
returned by typeid
can be equivalently seen as a
call to a virtual getter method which returns a statically allocated structure. The compiler adds
some fancy syntax around and automatically creates the type information for us, but using typeid
is just equal to:
struct MyRtti {
struct RttiData {
const char* name;
};
virtual ~MyRtti() = default;
virtual auto get_rtti() const -> const RttiData& = 0;
};
struct Base : MyRtti {
const RttiData& get_rtti() const override {
static MyRtti::RttiData info{ "Base" };
return info;
}
};
The compiler just saves all this hassle above from you.
In practice, however, the implementation above is optimized a bit by the compiler. Note that using a
virtual getter method which just returns the pointer to the sta