Why use std::type_index instead of std::type_info*

Learn why use std::type_index instead of std::type_info* with practical examples, diagrams, and best practices. Covers c++, c++11, rtti development techniques with visual explanations.

Why Use std::type_index Instead of std::type_info* in C++?

Hero image for Why use std::type_index instead of std::type_info*

Explore the advantages of std::type_index over raw std::type_info* pointers for type identification and comparison in C++, focusing on its utility in associative containers and RTTI.

In C++, Runtime Type Information (RTTI) allows programs to retrieve information about the type of an object during program execution. The primary mechanism for this is std::type_info, accessible via the typeid operator. While typeid returns a const std::type_info&, directly using std::type_info* for comparisons or as keys in associative containers can lead to unexpected behavior. This is where std::type_index comes into play, offering a robust and reliable solution for type identification.

The Challenge with std::type_info*

The typeid operator returns a reference to a std::type_info object. When you take the address of this reference (e.g., &typeid(T)), you get a const std::type_info*. The crucial point is that std::type_info objects are not guaranteed to be unique across different translation units or even different invocations of typeid for the same type. While std::type_info::operator== correctly compares types, comparing std::type_info* pointers directly using == only checks if they point to the exact same memory location, not if they represent the same type. This makes std::type_info* unsuitable as a key in std::map or std::unordered_map because these containers rely on pointer equality for their default comparison/hashing, not the semantic type equality provided by std::type_info::operator==.

flowchart TD
    A[typeid(T) returns const std::type_info&] --> B{Take address: const std::type_info*}
    B --> C1["Pointer comparison (ptr1 == ptr2)"]
    C1 -- "Checks memory address equality" --> D1[Unreliable for type identity]
    B --> C2["std::type_info::operator== (\*ptr1 == \*ptr2)"]
    C2 -- "Checks semantic type equality" --> D2[Reliable for type identity]
    D1 --> E["Problem: std::map/unordered_map use pointer equality by default"]
    D2 --> F["Solution: std::type_index provides value semantics"]
    E --> F

The problem with std::type_info* for type identity.

Introducing std::type_index

std::type_index (introduced in C++11) is a lightweight wrapper around const std::type_info*. Its primary purpose is to provide value semantics for std::type_info objects, making them suitable for use in standard library containers that require strict weak ordering (like std::map) or hashing (like std::unordered_map).

std::type_index achieves this by:

  1. Storing a const std::type_info*: It internally holds a pointer to the std::type_info object.
  2. Overloading comparison operators: It overloads operator==, operator<, etc., to delegate to the corresponding std::type_info member functions (std::type_info::before and std::type_info::operator==). This ensures that comparisons are based on the actual type, not the pointer value.
  3. Providing a hash function: It has a specialized std::hash for std::type_index, which delegates to std::type_info::hash_code(), making it usable as a key in std::unordered_map.
#include <iostream>
#include <typeinfo>
#include <typeindex>
#include <map>
#include <string>

class Base {};
class Derived : public Base {};

int main() {
    // Problem with raw type_info* as map key
    std::map<const std::type_info*, std::string> type_info_map_bad;
    // This might work on some compilers/platforms, but is not guaranteed
    // to correctly identify types across different typeid invocations or TUs.
    // The default comparison for map uses pointer comparison.
    type_info_map_bad[&typeid(int)] = "Integer";
    type_info_map_bad[&typeid(double)] = "Double";
    std::cout << "Bad map size: " << type_info_map_bad.size() << std::endl;
    // Accessing might fail if &typeid(int) yields a different pointer
    // even if it refers to the same type_info object.

    // Solution with std::type_index
    std::map<std::type_index, std::string> type_info_map_good;
    type_info_map_good[std::type_index(typeid(int))] = "Integer";
    type_info_map_good[std::type_index(typeid(double))] = "Double";
    type_info_map_good[std::type_index(typeid(Base))] = "Base Class";
    type_info_map_good[std::type_index(typeid(Derived))] = "Derived Class";

    std::cout << "Good map size: " << type_info_map_good.size() << std::endl;

    // Accessing elements using std::type_index is reliable
    std::cout << "Type of int: " << type_info_map_good[std::type_index(typeid(int))] << std::endl;
    std::cout << "Type of Derived: " << type_info_map_good[std::type_index(typeid(Derived))] << std::endl;

    // Comparing type_info* vs type_index
    const std::type_info* p_int1 = &typeid(int);
    const std::type_info* p_int2 = &typeid(int);
    std::cout << "\nComparing type_info*:\n";
    std::cout << "p_int1 == p_int2: " << (p_int1 == p_int2 ? "true" : "false") << std::endl; // May be true or false
    std::cout << "*p_int1 == *p_int2: " << (*p_int1 == *p_int2 ? "true" : "false") << std::endl; // Always true

    std::type_index ti_int1 = typeid(int);
    std::type_index ti_int2 = typeid(int);
    std::cout << "\nComparing std::type_index:\n";
    std::cout << "ti_int1 == ti_int2: " << (ti_int1 == ti_int2 ? "true" : "false") << std::endl; // Always true

    return 0;
}

Demonstrating std::type_index for reliable type comparison and use in std::map.

Key Benefits of std::type_index

The primary benefits of using std::type_index are:

  • Reliable Type Comparison: It guarantees that two std::type_index objects compare equal if and only if they represent the same type, regardless of whether their underlying std::type_info* pointers are identical.
  • Container Compatibility: It provides the necessary comparison and hashing mechanisms to be used directly as keys in std::map, std::unordered_map, std::set, and std::unordered_set without requiring custom comparators or hash functions.
  • Value Semantics: It behaves like a value type, making it easier to reason about and pass around in your code, unlike raw pointers which often imply ownership or specific memory locations.
  • Standard Library Integration: It's part of the C++ standard library (<typeindex>), ensuring portability and consistency across different compilers and platforms.
classDiagram
    class std_type_info {
        +name() : const char*
        +hash_code() : size_t
        +operator==(const std_type_info&) : bool
        +before(const std_type_info&) : bool
    }

    class std_type_index {
        -const std_type_info* _ptr
        +std_type_index(const std_type_info&)
        +operator==(const std_type_index&) : bool
        +operator<(const std_type_index&) : bool
        +hash_code() : size_t
        +name() : const char*
    }

    std_type_index "1" -- "1" std_type_info : "wraps pointer to"
    std_type_index ..> std_hash : "specialized for"

Class diagram illustrating std::type_index wrapping std::type_info.