Skip to content
C++
Library
since C++11
Intermediate

std::type_index

A copyable, hashable wrapper around std::type_info enabling runtime type information to be used as keys in associative containers.

std::type_indexsince C++11

A lightweight wrapper around a const std::type_info& that adds copy semantics, ordering operators, and a std::hash specialization, making runtime type information usable as a key in std::map and std::unordered_map.

Overview

std::type_info, obtained via the typeid operator, describes the dynamic type of an expression at runtime. It is non-copyable, non-default-constructible, and lacks the comparison operators needed by ordered containers. These constraints exist because type_info objects have static storage duration and are owned by the implementation β€” you are only ever meant to hold references or pointers to them.

std::type_index, introduced in C++11 via <typeindex>, resolves all of these usability gaps. It internally stores a const std::type_info* β€” a raw pointer that remains valid for the lifetime of the program β€” and exposes it through an interface that satisfies both the LessThanComparable and EqualityComparable requirements, plus provides std::hash<std::type_index> out of the box.

This combination makes std::type_index the standard mechanism for building type-keyed dispatch tables, factory registries, per-type callback maps, and any other structure that needs to branch on the runtime identity of a C++ type.

Syntax

cpp
#include <typeindex>   // C++11

namespace std {
    class type_index {
    public:
        type_index(const type_info& info) noexcept;   // C++11

        bool operator==(const type_index&) const noexcept;  // C++11
        bool operator!=(const type_index&) const noexcept;  // C++11; deprecated in C++20
        bool operator< (const type_index&) const noexcept;  // C++11
        bool operator<=(const type_index&) const noexcept;  // C++11
        bool operator> (const type_index&) const noexcept;  // C++11
        bool operator>=(const type_index&) const noexcept;  // C++11

        size_t     hash_code() const noexcept;  // C++11
        const char* name()    const noexcept;  // C++11; return value is implementation-defined
    };
}

template<>
struct std::hash<std::type_index>;  // C++11

Construction is always from a typeid() expression. There is no default constructor. Because typeid on a null pointer throws std::bad_typeid, always obtain a type_index from a valid expression or a dereferenced polymorphic pointer.

Examples

Basic container keying

cpp
#include <typeindex>
#include <unordered_map>
#include <map>
#include <string>
#include <iostream>

int main() {
    // std::unordered_map uses std::hash<std::type_index>  // C++11
    std::unordered_map<std::type_index, std::string> names;
    names[typeid(int)]         = "int";
    names[typeid(double)]      = "double";
    names[typeid(std::string)] = "std::string";

    int x = 42;
    auto it = names.find(typeid(x));
    if (it != names.end())
        std::cout << it->second << '\n';  // "int"

    // std::map uses operator<  // C++11
    std::map<std::type_index, int> sizes;
    sizes[typeid(char)]   = sizeof(char);
    sizes[typeid(int)]    = sizeof(int);
    sizes[typeid(double)] = sizeof(double);
}

Type-keyed factory registry

A common pattern in plugin systems and test frameworks is registering a factory function per concrete type, then instantiating by type identity at runtime.

cpp
#include <typeindex>
#include <unordered_map>
#include <functional>
#include <memory>
#include <stdexcept>

struct Shape { virtual ~Shape() = default; };
struct Circle  : Shape {};
struct Square  : Shape {};
struct Triangle: Shape {};

class ShapeFactory {
    using Factory = std::function<std::unique_ptr<Shape>()>;
    std::unordered_map<std::type_index, Factory> registry_;  // C++11

public:
    template<typename T>
    void register_type() {
        static_assert(std::is_base_of_v<Shape, T>, "T must derive from Shape");  // C++17
        registry_.emplace(typeid(T), [] { return std::make_unique<T>(); });      // C++14
    }

    template<typename T>
    std::unique_ptr<Shape> create() const {
        auto it = registry_.find(typeid(T));
        if (it == registry_.end())
            throw std::runtime_error("type not registered");
        return it->second();
    }
};

int main() {
    ShapeFactory factory;
    factory.register_type<Circle>();
    factory.register_type<Square>();

    auto c = factory.create<Circle>();  // returns unique_ptr<Shape>
}

Runtime event dispatcher

When building a heterogeneous event system, type_index allows O(1) handler lookup keyed on the dynamic type of the dispatched event.

cpp
#include <typeindex>
#include <unordered_map>
#include <functional>

struct Event         { virtual ~Event() = default; };
struct KeyEvent      : Event { int key; };
struct MouseMoveEvent: Event { int x, y; };
struct ResizeEvent   : Event { int w, h; };

class Dispatcher {
    std::unordered_map<
        std::type_index,
        std::function<void(const Event&)>
    > handlers_;  // C++11

public:
    template<typename E, typename F>
    void on(F&& handler) {
        handlers_[typeid(E)] = [h = std::forward<F>(handler)](const Event& e) {
            h(static_cast<const E&>(e));
        };
    }

    void dispatch(const Event& e) const {
        // typeid(e) yields the dynamic type because Event has virtual functions  // C++11
        auto it = handlers_.find(typeid(e));
        if (it != handlers_.end())
            it->second(e);
    }
};

int main() {
    Dispatcher d;
    d.on<KeyEvent>([](const KeyEvent& e) { /* handle key */ (void)e.key; });
    d.on<ResizeEvent>([](const ResizeEvent& e) { /* handle resize */ (void)e.w; });

    KeyEvent ke; ke.key = 65;
    d.dispatch(ke);  // routes to KeyEvent handler
}

Best Practices

Use typeid(*ptr) not typeid(ptr) for polymorphic dispatch. typeid applied to a pointer expression yields the static type of the pointer (Base*), not the dynamic type of the pointee. Dereference first: typeid(*ptr) triggers the virtual dispatch that gives you the runtime type. This only works when the pointed-to type has at least one virtual function.

Prefer std::unordered_map over std::map for type dispatch tables. hash_code() returns a stable value within a single program run, and the hash-based lookup is O(1) vs O(log n) for the ordered map. The ordering imposed by operator< on type_index is unspecified β€” it is consistent within a run, but meaningless across runs or platforms, so there is no semantic reason to choose map unless you need iteration in stable order.

Build the registry at startup, read it at runtime. Registries populated during static initialization (static local or global objects) can be read safely from multiple threads once initialization is complete. Writing to the map from multiple threads without synchronization is a data race. A common pattern is to lock the registry only during the registration phase and then treat it as read-only.

Cache the type_index for hot paths. typeid itself involves a pointer dereference and is fast, but if you are calling dispatch(e) in a tight loop, precomputing a local std::type_index idx = typeid(e) and using that avoids any redundant indirection.

Common Pitfalls

name() is not portable. The string returned by type_index::name() is implementation-defined. On GCC and Clang it is mangled (Pi for int*); on MSVC it is human-readable. Never use name() for serialization, logging that must be cross-platform, or comparison β€” hash_code() and operator== are the right tools for comparison.

Null pointer typeid throws. typeid(*ptr) where ptr is a null pointer to a polymorphic type throws std::bad_typeid at runtime. Guard dereferences or use non-polymorphic typeid(T) where possible.

Type identity is per-program, not per-ABI. On platforms with multiple shared libraries, two type_index values representing the same type but obtained in different shared objects may compare unequal if the linker fails to merge the type_info symbols. This is a known issue on some POSIX platforms when libraries are loaded with RTLD_LOCAL. Use RTLD_GLOBAL or ensure RTTI symbols are properly exported.

type_index does not survive process boundaries. hash_code() values and pointer-based identity are only stable within a single program run. Do not serialize type_index values to disk or pass them across processes or network connections.

See Also

  • typeid operator β€” source of all type_info and type_index values; defined in <typeinfo>
  • std::any (C++17) β€” type-erased value container that uses type_index internally for type() queries
  • std::variant + std::visit (C++17) β€” compile-time type dispatch that avoids RTTI entirely when the type set is closed