When a function receives a Product* that might point to a DigitalProduct, a PhysicalProduct, or something else entirely, C++ provides two tools to ask "what is this object actually?" at runtime. Those tools are typeid and dynamic_cast, both backed by Run-Time Type Information. This chapter covers what RTTI is, how to use each tool, what they cost, and when to skip them in favor of a virtual function.
The compiler usually erases type information once it has finished checking the code. By the time the program runs, a Product* is an address. The bytes at that address do not carry a label saying "I'm a DigitalProduct."
RTTI changes that, but only for classes that have at least one virtual function. For polymorphic classes, the compiler emits metadata for each class and links it through the vtable. At runtime, given a pointer or reference to one of those objects, the program can follow the vtable, find the metadata, and answer questions like "is this a DigitalProduct?" or "what is the real name of this type?"
That is all RTTI is: a side table of type descriptions, reachable through the vtable, available for polymorphic objects only.
The diagram shows the path. Every polymorphic object has a vptr pointing at its class's vtable. The vtable holds function pointers for the virtual methods plus a slot referring to the class's type_info. typeid reads that slot. dynamic_cast walks the class hierarchy starting from there.
Classes without any virtual function do not get this metadata. They have no vtable, so there is nowhere to hang it. That is why both typeid (in its interesting form) and dynamic_cast require a polymorphic base class to do real work.
RTTI adds a small amount of per-class metadata to the binary (a type_info object and a name string per polymorphic class). The runtime cost is paid only when typeid or dynamic_cast is called. Programs that never use either feature pay no runtime cost beyond the metadata sitting in the binary.
typeid and std::type_infotypeid(expr) returns a reference to a std::type_info object describing the type of expr. Include <typeinfo> to use it. The type_info object supports comparing types with == and !=, and asking for a name with .name().
Two things to note. First, both typeid(ebook) and typeid(asBase) report DigitalProduct, even though asBase is declared as a Product&. That is RTTI doing its job: it followed the vtable and found the real type. Second, .name() returns an implementation-defined C-string. g++ and clang++ produce mangled names like 14DigitalProduct (the 14 is the length of DigitalProduct). MSVC produces something more readable. Do not depend on the format.
For a demangled name on g++ or clang++, the runtime offers abi::__cxa_demangle in <cxxabi.h>, but that is outside the standard. For type comparisons, use == on the type_info references directly.
This works, but it is almost always a sign that a virtual function would be clearer. The "When Not to Use RTTI" section covers this further.
typeidtypeid has two modes, and the difference matters. The mode is chosen based on the argument.
Argument to typeid | What you get |
|---|---|
| A glvalue of a polymorphic type | The dynamic type of the object (RTTI lookup) |
| A glvalue of a non-polymorphic type | The static type (no runtime work) |
Any type name, like typeid(int) | The static type (no runtime work) |
A non-glvalue expression, like typeid(someFunction()) returning a value | The static type |
"Glvalue" is the C++ term for an expression that refers to an object whose address can be taken, more or less. Variables, dereferenced pointers, and references are all glvalues. A function call that returns by value is not.
The case where RTTI does real work is one specific pattern: typeid(*ptr) or typeid(ref) where the underlying class is polymorphic. Everything else is resolved at compile time.
polyRef is declared as Product& but refers to a DigitalProduct. Because Product is polymorphic, typeid does the runtime lookup and finds DigitalProduct. plainRef is declared as Customer& and refers to a PremiumCustomer, but Customer has no virtual functions, so typeid reports the static type Customer. The runtime never gets a chance to look at the real object.
This is a real bug source. Using typeid while forgetting that the base class is not polymorphic produces wrong answers without warning.
typeid on a polymorphic glvalue performs a vtable lookup and a couple of pointer reads. It is measured in nanoseconds, but it is not free. typeid(int) or typeid(somePodStruct) is free, the result is baked in at compile time.
typeid on a Null PointerOne runtime error case is worth knowing. typeid(*p) where p is a null pointer to a polymorphic type makes the runtime throw std::bad_typeid. The compiler has to read the vtable through *p to answer the question, and it cannot.
Two clarifications. First, typeid(p) (without the star) is fine and reports Product*, because that is the static type of the pointer itself. Only dereferencing through typeid(*p) triggers the check. Second, this only throws for polymorphic types. typeid(*nullIntPtr) on a non-polymorphic type is undefined behavior, not a bad_typeid, because there is nothing to look up at runtime.
Code that catches bad_typeid is rarely useful. Check for null before calling typeid on a dereferenced pointer.
dynamic_cast for Safe Downcastingtypeid is useful for asking exactly what type something is. dynamic_cast is useful for using that type. It performs a downcast (or cross-cast, covered later) and reports, at runtime, whether the cast was valid.
The basic chapter on static_cast introduced dynamic_cast<Derived*>(basePtr) briefly. The full rules:
dynamic_cast<Derived*>(basePtr) returns a Derived* if basePtr points to a Derived (or a class derived from Derived). Otherwise it returns nullptr.static_cast. The runtime walks class hierarchy information.The standard safe-downcast pattern:
The if (auto* digital = dynamic_cast<DigitalProduct*>(item)) idiom does two things in one line: it performs the cast and checks the result. If the cast returned null, the condition is false and the block is skipped. This is the idiomatic shape, and it scopes the cast result to exactly the block where it is valid.
The diagram shows the two paths. dynamic_cast returns either a usable pointer or nullptr, and the calling code branches accordingly. The pointer form does not throw, only a null check is needed.
dynamic_cast walks the class hierarchy at runtime and is noticeably slower than static_cast, especially in deep or wide hierarchies. In a hot loop, prefer virtual dispatch. As a rough rule, dynamic_cast is around an order of magnitude slower than static_cast, though numbers vary by compiler and hierarchy shape.
dynamic_cast to ReferencesThe pointer form of dynamic_cast returns nullptr on failure. References cannot be null, so the reference form fails differently: it throws std::bad_cast.
Which form to use depends on failure handling.
| Use the pointer form when | Use the reference form when |
|---|---|
| The cast might legitimately fail and branching on it is needed | A wrong type is a programmer error and failure should be loud |
| Scanning a heterogeneous collection | The type has already been asserted elsewhere |
| Performance matters and exception cost should be avoided | Exceptions are an acceptable error channel in the codebase |
Both forms do the same work internally. The only difference is how they report failure. Pick the one that matches the error-handling style at the call site.
dynamic_cast can also cross-cast between two base classes that share a common derived class. This applies with multiple inheritance, the only setting where the operation is meaningful.
Consider a Reviewable mixin that adds review-handling and a Shippable mixin that adds shipping. A PhysicalProduct inherits from both. Given a Reviewable* that points to a PhysicalProduct, a Shippable* may be needed for the shipping part of the code.
Reviewable and Shippable are unrelated. A static_cast between them does not compile. But because the underlying object is a PhysicalProduct that inherits from both, dynamic_cast can walk down to the most-derived object and then back up to the other base. The cast adjusts the pointer to point at the right sub-object.
The takeaway: cross-casts are a niche feature that exists for multiple inheritance scenarios. In single inheritance hierarchies, there is nothing to cross-cast to.
dynamic_cast<void*>One specialized form is worth mentioning. dynamic_cast<void*>(polyPtr) returns a void* pointing to the most-derived complete object, regardless of which sub-object polyPtr was pointing into.
reviewable and shippable point at different sub-objects of the same PhysicalProduct, so they have different addresses. dynamic_cast<void*> collapses them both to the address of the whole PhysicalProduct. The use cases are narrow: comparing whether two base pointers refer to the same complete object, or storing a stable identity in a debugging table.
For day-to-day code, this form is rare. Knowing it exists is enough so the spelling does not surprise readers in a code review.
dynamic_cast walks the class hierarchy at runtime. The walk is short for shallow single-inheritance trees and longer for deep or multiply-inherited ones. It is always slower than calling a virtual function, which is one indirect call through the vtable.
That cost matters in tight loops. It matters less for code paths that run once per request. The bigger reason to prefer virtual functions: the design tends to be clearer. When the code starts to look like this:
Every time a new product type is added, postPurchase has to be updated. The compiler does not warn about a missing branch. The function is also doing two jobs: figuring out what type something is, and acting on that type.
The polymorphic version replaces the branching with a virtual function:
Adding a new subtype now means writing one new class and overriding postPurchaseAction. The call site does not change. The compiler enforces that every concrete Product provides the behavior (especially if postPurchaseAction is pure virtual).
The rule: if typeid or dynamic_cast is being used to branch on type and call type-specific behavior, a virtual function is usually a better fit. RTTI is appropriate when the behavior cannot be expressed as a method call.
A virtual function call is one indirect jump through the vtable. dynamic_cast walks hierarchy metadata and is roughly an order of magnitude more expensive. Replacing a dynamic_cast chain with a single virtual call usually wins on both clarity and speed.
RTTI is not a code smell on its own. There are real cases where virtual dispatch cannot help and typeid or dynamic_cast is the clearer option.
Crossing the boundary between two type hierarchies that cannot share a base. Integrating an analytics library that has its own Event hierarchy alongside code that has a Product hierarchy. Virtual methods cannot be added to the analytics types, and Product types cannot inherit from theirs. At the seam, dynamic_cast or typeid is the direct way to figure out what each side has.
Serialization frameworks. Saving a polymorphic object to disk means writing out a tag that says which class it is. Loading reads the tag and constructs the right type. Frameworks like Boost.Serialization use typeid and a registry of factories to do this. A virtual serialize method handles the per-class work, but identifying which class to instantiate on load needs type identity.
Error message formatting and debug logging. Printing "operation failed on a DigitalProduct" in a log uses typeid(*p).name() to get the type name without changing every class to override a typeName() method. The mangled name is ugly but workable.
Integrating with code you do not own. Plugin systems that hand a pointer of one base type but expect a check for specific interfaces use dynamic_cast at the boundary. COM-style QueryInterface is essentially dynamic_cast under a different name.
The common pattern: RTTI fits where the type-specific decision is at the edge of a system, not in the business logic. If the decision is "should this thing be downloaded or shipped", that is business logic, and a virtual function is clearer. If the decision is "what kind of object did the framework hand me", that is a boundary problem, and RTTI is reasonable.
-fno-rttig++ and clang++ both accept -fno-rtti, which compiles the program without RTTI support. MSVC's equivalent is /GR-. The motivation is binary size: every polymorphic class would otherwise carry a type_info object and a name string, and large codebases can save several percent by turning RTTI off.
LLVM, parts of Chrome, and a few game engines build with RTTI disabled. They commit to not using typeid on polymorphic types and not using the polymorphic form of dynamic_cast. Internal hierarchies that need type identity roll their own, often with a manual enum tag or a static kind field per class.
Changes with -fno-rtti:
| Feature | With RTTI (default) | Without RTTI (-fno-rtti) |
|---|---|---|
typeid on non-polymorphic types | Works, compile-time | Works, compile-time |
typeid on polymorphic glvalues | Works, runtime lookup | Compile error |
dynamic_cast<Derived*>(basePtr) | Works, runtime check | Compile error |
dynamic_cast<void*>(polyPtr) | Works | Compile error |
| Virtual function calls | Work | Work, unchanged |
| Exception handling | Works | Works, but throwing some types depends on RTTI internally |
The last row is the wrinkle. catch (const SomeException& e) matches based on type identity, and the runtime uses RTTI internally. Compilers like g++ do enough special-casing that exceptions still work with -fno-rtti, but the catch-by-base-class matching for user-defined hierarchies gets more restrictive. If the code throws and catches many custom exception types, test carefully before enabling the flag.
For application code, leave RTTI on. The cost is real but small, and the features it enables (typeid, dynamic_cast, standard exception machinery) are worth more than the binary size saved. The flag exists for codebases with specific constraints. Enable it only after measuring a problem.
A small example that combines typeid and dynamic_cast for a realistic case: a checkout function that handles a heterogeneous list of products.
This is a fair use of dynamic_cast. The fulfillment logic depends on capabilities that only some subtypes have. A digital product has a download URL, a physical product has a warehouse, and a plain Product has neither. The dispatch could be replaced with a virtual fulfill() method, and in a real codebase that is usually the better answer. But when the per-type behavior is short and the hierarchy is small, the dynamic_cast version is direct enough that the redesign is not worth it.
The honest version of the rule: prefer virtual functions for type-dependent behavior, use RTTI when crossing system boundaries or doing one-off type queries. Both are valid tools. The skill is recognizing which one fits.
10 quizzes