Page Content

Tutorials

RTTI Run-Time Type Information In C++’s Run-Time Insights

RTTI Run-Time Type Information in C++

RTTI in C++ lets you detect an object’s type while the program is running. This feature is critical for polymorphic settings since they may not know the exact type of a base-class pointer or reference at build time.

Runtime type information is unnecessary in non-polymorphic languages like C since object types are fixed at compile time. C++’s object-oriented paradigm allows base-class pointers to point to the base class’s objects or any derived class. In order to remedy this, RTTI makes type identification available at runtime.

Components

The dynamic_cast operator and the typeid operator are the two main parts of RTTI.

typeid operator

Use typeid to enquire “What type is your object?” Returns std::type_info for the expression. Typeid often needs the header file.

Key points about typeid:

  • Typeid returns the expression’s dynamic type the object’s true type at runtime when applied to a type with virtual functions.
  • If the reference, pointer, or object has no virtual functions, typeid returns its static type (compile-time type).
  • Template classes can use typeid.
  • Its expression does not directly accept reference types; for instance, typeid(int& refptrTest) would report int.

Code Example for typeid:

#include <iostream>
#include <typeinfo> // Required for typeid and type_info
class Base {
public:
    // A virtual function is needed for typeid to return dynamic type for Base* or Base&
    virtual void print() const {
        std::cout << "I am Base" << std::endl;
    }
};
class Derived1 : public Base {
public:
    void print() const override { // 'override' (C++11) explicitly marks it as overriding a virtual function
        std::cout << "I am Derived1" << std::endl;
    }
};
class Derived2 : public Base {
public:
    void print() const override {
        std::cout << "I am Derived2" << std::endl;
    }
    void specificFunction() const {
        std::cout << "Derived2 specific function" << std::endl;
    }
};
class NonPolymorphic {
public:
    void greet() const {
        std::cout << "Hello from NonPolymorphic" << std::endl;
    }
};
int main() {
    Base b_obj;
    Derived1 d1_obj;
    Derived2 d2_obj;
    NonPolymorphic np_obj;
    Base* p_base; // Base class pointer
    // Using typeid with objects directly
    std::cout << "Type of b_obj: " << typeid(b_obj).name() << std::endl; // Output: Base
    std::cout << "Type of d1_obj: " << typeid(d1_obj).name() << std::endl; // Output: Derived1
    std::cout << "Type of np_obj: " << typeid(np_obj).name() << std::endl; // Output: NonPolymorphic
    std::cout << "\n--- Using typeid with polymorphic types via base pointer ---" << std::endl;
    p_base = &b_obj;
    std::cout << "p_base pointing to Base, typeid(*p_base): " << typeid(*p_base).name() << std::endl; // Output: Base
    p_base = &d1_obj;
    std::cout << "p_base pointing to Derived1, typeid(*p_base): " << typeid(*p_base).name() << std::endl; // Output: Derived1
    p_base = &d2_obj;
    std::cout << "p_base pointing to Derived2, typeid(*p_base): " << typeid(*p_base).name() << std::endl; // Output: Derived2
    std::cout << "\n--- Using typeid with non-polymorphic types via pointer ---" << std::endl;
    NonPolymorphic* p_np = &np_obj;
    // For non-polymorphic types, typeid(*pointer) gives static type, not dynamic.
    // If the class had no virtual functions, typeid(p_base) would also give static type, i.e., "Base*"
    std::cout << "p_np pointing to NonPolymorphic, typeid(*p_np): " << typeid(*p_np).name() << std::endl; // Output: NonPolymorphic
    std::cout << "Type of p_np (the pointer itself): " << typeid(p_np).name() << std::endl; // Output: NonPolymorphic*
    return 0;
}

Output

Type of b_obj: 4Base
Type of d1_obj: 8Derived1
Type of np_obj: 14NonPolymorphic
--- Using typeid with polymorphic types via base pointer ---
p_base pointing to Base, typeid(*p_base): 4Base
p_base pointing to Derived1, typeid(*p_base): 8Derived1
p_base pointing to Derived2, typeid(*p_base): 8Derived2
--- Using typeid with non-polymorphic types via pointer ---
p_np pointing to NonPolymorphic, typeid(*p_np): 14NonPolymorphic
Type of p_np (the pointer itself): P14NonPolymorphic

dynamic_cast operator

Polymorphic types are safely downcast using the dynamic_cast operator. This implies that it enables you to change a base class pointer or reference to a derived class pointer or reference. Runtime is used to execute this cast, and its security is verified.

Key points about dynamic_cast:

Only polymorphic types (classes with at least one virtual function) can utilise it.

Syntax

target_type* result_ptr = dynamic_cast<target_type*>(pointer_expression);
target_type& result_ref = dynamic_cast<target_type&>(reference_expression);

Failure Handling:

  • A nullptr is returned if dynamic_cast is used with pointers and the cast fails (that is, the object being referenced to is not of the target_type or a type derived from it).
  • An std::bad_cast exception is raised if dynamic_cast is used with references and the cast fails. Dynamic_cast with references is appropriate for failed casts.

If the object does not match the target derived type, C-style casts or static_cast may cause severe runtime issues for downcasting.

Code Example for dynamic_cast:

The generic form of dynamic_cast.

object of target type = dynamic_cast<target type>(pointer expression);
object of target type = dynamic_cast<target type>(reference expression);
Specifically, for a reference cast and its exception handling:
void f(const Base &b) {
    try {
        const Derived &d = dynamic_cast<const Derived&>(b);
        // use the Derived object to which b referred 
    } catch (std::bad_cast) {
        // handle the fact that the cast failed 
    }
}

Expanding on this with a full example:

#include <iostream>
#include <typeinfo> // For dynamic_cast and type_info
class Base {
public:
    virtual void greet() const { // Virtual function makes Base polymorphic
        std::cout << "Hello from Base!" << std::endl;
    }
};
class DerivedA : public Base {
public:
    void greet() const override {
        std::cout << "Hello from DerivedA!" << std::endl;
    }
    void funcA() const {
        std::cout << "DerivedA's specific function." << std::endl;
    }
};
class DerivedB : public Base {
public:
    void greet() const override {
        std::cout << "Hello from DerivedB!" << std::endl;
    }
    void funcB() const {
        std::cout << "DerivedB's specific function." << std::endl;
    }
};
int main() {
    Base* ptr_base = nullptr;
    // Case 1: Pointing to DerivedA
    DerivedA objA;
    ptr_base = &objA;
    // Try to downcast to DerivedA*
    if (DerivedA* ptr_a = dynamic_cast<DerivedA*>(ptr_base)) {
        ptr_a->greet();
        ptr_a->funcA();
    } else {
        std::cout << "Cast to DerivedA* failed." << std::endl;
    }
    // Try to downcast to DerivedB* (should fail)
    if (DerivedB* ptr_b = dynamic_cast<DerivedB*>(ptr_base)) {
        ptr_b->greet();
        ptr_b->funcB();
    } else {
        std::cout << "Cast to DerivedB* failed as expected." << std::endl;
    }
    std::cout << "\n--- Using dynamic_cast with references ---" << std::endl;
    // Case 2: Using references
    Base& ref_base = objA;
    try {
        DerivedA& ref_a = dynamic_cast<DerivedA&>(ref_base);
        ref_a.greet();
        ref_a.funcA();
    } catch (const std::bad_cast& e) {
        std::cerr << "Reference cast to DerivedA& failed: " << e.what() << std::endl;
    }
    try {
        // This cast will fail and throw std::bad_cast because ref_base refers to DerivedA, not DerivedB
        DerivedB& ref_b = dynamic_cast<DerivedB&>(ref_base);
        ref_b.greet();
        ref_b.funcB();
    } catch (const std::bad_cast& e) {
        std::cerr << "Reference cast to DerivedB& failed as expected: " << e.what() << std::endl;
    }
    return 0;
}

Output

Hello from DerivedA!
DerivedA's specific function.
Cast to DerivedB* failed as expected.
--- Using dynamic_cast with references ---
Hello from DerivedA!
DerivedA's specific function.
Reference cast to DerivedB& failed as expected: std::bad_cast

Limitations and Misuses of RTTI

Despite its strength, RTTI has many drawbacks and is frequently dissuaded from being used as the main design tool.

Requires Polymorphic Classes: Classes with at least one virtual function are the only ones that RTTI can use. Typeid will only display a class’s static type if it lacks virtual functions, and dynamic_cast will not be functional.

Compiler-Dependent Activation: Given the extra data that must be kept for runtime type checks, certain compilers may need a particular switch to allow RTTI.

Error-Prone Compared to Virtual Functions: When compared to virtual member functions, RTTI operators are often “more error-prone,”. Programmers are required to manually handle errors and verify that the cast is proper.

Design Preference: C++ emphasises templates and polymorphism (virtual functions) as generally better options than RTTI. These techniques frequently result in more robust designs and improved static type checking. Use of RTTI as a “shortcut around proper, more robust design” is not advised.

Avoid Switch Statements: RTTI can be abused to create “thinly disguised switch-statements” by implementing logic that is better handled by virtual functions.

Appropriate Use Cases for RTTI

Notwithstanding its limitations, RTTI has valid uses in which it may be advantageous:

Runtime Type Determination: With RTTI, the program can dynamically detect and act upon an object’s type when its exact type is truly unknown at compile time.

Equality Comparison in Hierarchies: To guarantee that two objects have the same dynamic type before doing a thorough comparison using a virtual equality function, RTTI (typeid) can be utilised when designing an equality operator for a class hierarchy.

Navigating Class Hierarchies: Since dynamic_cast offers a type-safe method of directly moving down the inheritance chain, it is crucial when class hierarchy navigation cannot be avoided.

Extending Library Classes: In situations when it is not possible to add virtual functions to existing base classes (for example, from a third-party library), RTTI may be required in order to add additional functionality through derivation.

Object I/O Systems: Typeid can be used to identify an object’s true type in complex object serialisation so that it can be written out appropriately, particularly when linked data structures are involved.

You can also read String In C++: Techniques For Efficient Text Handling

Agarapu Geetha
Agarapu Geetha
My name is Agarapu Geetha, a B.Com graduate with a strong passion for technology and innovation. I work as a content writer at Govindhtech, where I dedicate myself to exploring and publishing the latest updates in the world of tech.
Index