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
}

Per-component type registry (thread-safe)

A single global registry shared across threads requires careful synchronization. The write-rarely, read-often pattern maps naturally to std::shared_mutex (C++17): registration takes an exclusive write lock, while every lookup takes a shared read lock, allowing unlimited concurrent readers once the registry is fully populated.

cpp
#include <typeindex>
#include <unordered_map>
#include <functional>
#include <memory>
#include <shared_mutex>   // C++17
#include <stdexcept>

// Base type for all creatable components
struct Component { virtual ~Component() = default; };

class ComponentRegistry {
public:
    using Creator = std::function<std::unique_ptr<Component>()>;

    // Register a concrete type T with its default constructor.
    // Typically called once at program startup from each translation unit.  // C++17
    template<typename T>
    void register_creator() {
        static_assert(std::is_base_of_v<Component, T>,
                      "T must derive from Component");          // C++17
        std::unique_lock lock(mutex_);                         // exclusive write lock
        creators_.emplace(typeid(T), [] {
            return std::make_unique<T>();                      // C++14
        });
    }

    // Look up and invoke the factory for a runtime type_index.
    // Safe to call from any thread once registration is complete.  // C++17
    std::unique_ptr<Component> create(std::type_index idx) const {
        std::shared_lock lock(mutex_);                         // shared read lock
        auto it = creators_.find(idx);
        if (it == creators_.end())
            throw std::runtime_error("type not registered");
        return it->second();
    }

    // Convenience: create by static type.
    template<typename T>
    std::unique_ptr<Component> create() const {
        return create(typeid(T));
    }

    static ComponentRegistry& global() {
        static ComponentRegistry instance;                     // C++11 (Meyers singleton)
        return instance;
    }

private:
    mutable std::shared_mutex mutex_;                          // C++17
    std::unordered_map<std::type_index, Creator> creators_;   // C++11
};

// --- Usage ---
struct AudioComponent : Component {};
struct PhysicsComponent : Component {};

// Registration can happen in any translation unit at startup.
void register_all() {
    auto& reg = ComponentRegistry::global();
    reg.register_creator<AudioComponent>();
    reg.register_creator<PhysicsComponent>();
}

int main() {
    register_all();

    // Many threads can call create() concurrently after this point.
    auto audio = ComponentRegistry::global().create<AudioComponent>();
}

The std::shared_mutex pattern is sometimes replaced with a two-phase design: populate the registry synchronously at startup (single-threaded), then freeze it. This avoids lock overhead entirely on the hot read path, at the cost of a stricter initialization protocol.

Best Practice: prefer the freeze-after-init pattern for performance-critical dispatch tables. If registrations always happen before the first create() call, an atomic flag or a simple bool ready checked once lets you drop the shared lock entirely from the read path after that flag is set.

std::any interop

std::any (C++17) stores a value of any copy-constructible type alongside its type_info. The member function std::any::type() returns a const std::type_info&, which you can wrap directly in std::type_index for O(1) dispatch on the runtime type of the stored value. This makes it straightforward to build heterogeneous message queues where each message type has a separately registered handler.

cpp
#include <any>          // C++17
#include <typeindex>
#include <unordered_map>
#include <functional>
#include <vector>
#include <iostream>

// --- Handler registry keyed by the type stored inside std::any ---  // C++17
class AnyDispatcher {
    using Handler = std::function<void(const std::any&)>;
    std::unordered_map<std::type_index, Handler> handlers_;  // C++11

public:
    // Register a handler for messages of type T.
    template<typename T, typename F>
    void on(F&& fn) {
        handlers_[typeid(T)] = [f = std::forward<F>(fn)](const std::any& a) {
            f(std::any_cast<const T&>(a));   // safe: we matched the type  // C++17
        };
    }

    // Dispatch a single message. std::any::type() gives us the stored type_info.  // C++17
    void dispatch(const std::any& msg) const {
        std::type_index idx(msg.type());    // wraps type_info in a hashable key  // C++17
        auto it = handlers_.find(idx);
        if (it != handlers_.end())
            it->second(msg);
        // silently drop unhandled message types
    }
};

// --- Message types ---
struct LoginMessage  { std::string user; };
struct LogoutMessage { std::string user; };
struct ChatMessage   { std::string user, text; };

int main() {
    AnyDispatcher bus;

    bus.on<LoginMessage>([](const LoginMessage& m) {
        std::cout << m.user << " logged in\n";
    });
    bus.on<LogoutMessage>([](const LogoutMessage& m) {
        std::cout << m.user << " logged out\n";
    });
    bus.on<ChatMessage>([](const ChatMessage& m) {
        std::cout << m.user << ": " << m.text << '\n';
    });

    // Heterogeneous message queue β€” all stored as std::any  // C++17
    std::vector<std::any> queue = {
        LoginMessage{"alice"},
        ChatMessage{"alice", "hello world"},
        LogoutMessage{"alice"},
    };

    for (const auto& msg : queue)
        bus.dispatch(msg);
}

The key line is std::type_index idx(msg.type()): msg.type() returns the type_info of whatever is stored, and constructing a type_index from it produces the hashable key used for O(1) handler lookup.

Common Pitfall: std::any::type() returns typeid(void) for an empty any. Always check msg.has_value() before dispatching, or guard the handler lookup against the typeid(void) sentinel β€” otherwise an empty any in the queue will silently fail a handler lookup, which may mask programming errors.

Visitor pattern vs type_index dispatch

Both std::visit + std::variant and std::type_index dispatch solve the "call different code depending on the runtime type of an object" problem. They serve different points in the design space.

cpp
// --- APPROACH A: std::variant + std::visit (closed type set)  // C++17 ---
#include <variant>
#include <iostream>

struct Circle  { double radius; };
struct Square  { double side; };
struct Triangle{ double base, height; };

using Shape = std::variant<Circle, Square, Triangle>;  // C++17

double area(const Shape& s) {
    return std::visit([](const auto& sh) -> double {  // C++17
        using T = std::decay_t<decltype(sh)>;
        if constexpr (std::is_same_v<T, Circle>)
            return 3.14159 * sh.radius * sh.radius;
        else if constexpr (std::is_same_v<T, Square>)
            return sh.side * sh.side;
        else
            return 0.5 * sh.base * sh.height;
    }, s);
}

Adding a new shape to the variant requires editing the Shape alias and recompiling every visitor. This is ideal when the set of types is fixed and known at compile time: the compiler enforces exhaustiveness and generates efficient jump tables.

cpp
// --- APPROACH B: type_index dispatch (open type set)  // C++11 ---
#include <typeindex>
#include <unordered_map>
#include <functional>
#include <memory>

struct ShapeBase { virtual ~ShapeBase() = default; };
struct Circle2   : ShapeBase { double radius; };
struct Square2   : ShapeBase { double side; };

class AreaCalculator {
    std::unordered_map<
        std::type_index,
        std::function<double(const ShapeBase&)>
    > table_;  // C++11

public:
    template<typename T>
    void register_handler(std::function<double(const T&)> fn) {
        table_[typeid(T)] = [fn](const ShapeBase& b) {
            return fn(static_cast<const T&>(b));
        };
    }

    double area(const ShapeBase& s) const {
        auto it = table_.find(typeid(s));   // dynamic type via virtual  // C++11
        if (it == table_.end()) return 0.0;
        return it->second(s);
    }
};

With type_index dispatch, a plugin loaded at runtime can call register_handler<PluginShape>(...) without recompiling the core. The trade-off is that there is no compile-time exhaustiveness check.

Decision table:

Criterionstd::variant + std::visitstd::type_index dispatch
Type set known at compile timeYes β€” preferredWorks, but overkill
Types added at runtime (plugins)NoYes
Exhaustiveness enforced by compilerYesNo
OverheadZero (stack allocation, jump table)unordered_map lookup + std::function call
Cross-module extensibilityRequires recompilationSupported via registration

Choose std::variant + std::visit for tightly-bounded, compile-time-known sets such as AST node types or configuration variants. Choose std::type_index dispatch when the type set is open β€” plugin architectures, extensible serializers, or any system where third-party code must inject new behavior without touching existing sources.

Demangling type names

type_index::name() returns an implementation-defined raw string. On GCC and Clang this is an ABI-mangled name (N3foo3BarE for foo::Bar); on MSVC it is already human-readable. To produce consistent, readable output in logging and diagnostics, demangle the string at the point of use.

cpp
#include <typeindex>
#include <string>
#include <cstdlib>    // std::free

// Platform-specific demangling  // C++11
#if defined(__GNUG__) || defined(__clang__)
#  include <cxxabi.h>

inline std::string demangle(const char* mangled) {
    int status = 0;
    // abi::__cxa_demangle allocates; caller must free()  // GCC/Clang
    char* realname = abi::__cxa_demangle(mangled, nullptr, nullptr, &status);
    std::string result = (status == 0 && realname) ? realname : mangled;
    std::free(realname);
    return result;
}

#elif defined(_MSC_VER)
#  include <windows.h>
#  include <dbghelp.h>
#  pragma comment(lib, "dbghelp.lib")

inline std::string demangle(const char* mangled) {
    char buf[1024] = {};
    // UnDecorateSymbolName strips MSVC decoration  // MSVC
    DWORD len = UnDecorateSymbolName(mangled, buf, sizeof(buf),
                                     UNDNAME_COMPLETE);
    return len > 0 ? std::string(buf) : std::string(mangled);
}

#else
// Fallback: return the raw name as-is on unknown platforms
inline std::string demangle(const char* mangled) {
    return mangled;
}
#endif

// Portable helper: returns a human-readable type name for T  // C++11
template<typename T>
std::string type_name() {
    return demangle(typeid(T).name());
}

// --- Usage ---
#include <iostream>
#include <vector>
#include <map>

int main() {
    std::cout << type_name<int>()                          << '\n'; // "int"
    std::cout << type_name<std::vector<int>>()             << '\n'; // "std::vector<int, ...>"
    std::cout << type_name<std::map<std::string, double>>()<< '\n'; // "std::map<...>"
}

The abi::__cxa_demangle function is part of the Itanium C++ ABI and is available on all GCC and Clang builds that have <cxxabi.h>. It heap-allocates the result, so the std::free call is mandatory to avoid a leak.

Common Pitfall: do not use type_name<T>() for equality comparisons or map keys. The demangled string is for human consumption only. Always use std::type_index for identity comparisons β€” demangled names are not guaranteed to be unique across all compilers, and the same type may have different string representations in debug vs. release builds.

is_polymorphic guard

typeid(*ptr) only yields the dynamic (runtime) type when the pointed-to type is polymorphic β€” that is, when it has at least one virtual function. Calling typeid(*ptr) on a non-polymorphic type silently returns the static type of the pointer, which is almost never what the caller wants. A compile-time guard makes this mistake a hard error rather than a subtle bug.

cpp
#include <typeindex>
#include <type_traits>
#include <stdexcept>

// C++17 approach: static_assert inside the helper
template<typename T>
std::type_index dynamic_type_index(const T* ptr) {
    static_assert(std::is_polymorphic_v<T>,          // C++17
        "dynamic_type_index requires a polymorphic type "
        "(T must have at least one virtual function). "
        "Use typeid(T) for static type lookup.");
    if (!ptr)
        throw std::bad_typeid{};
    return std::type_index(typeid(*ptr));             // C++11
}

// C++20 approach: requires clause β€” cleaner error message
template<typename T>
    requires std::is_polymorphic_v<T>                // C++20
std::type_index dynamic_type_index_v2(const T* ptr) {
    if (!ptr) throw std::bad_typeid{};
    return std::type_index(typeid(*ptr));             // C++11
}

// --- Illustration of the silent-failure case ---
struct NonPoly { int x; };         // no virtual functions
struct Poly { virtual ~Poly() {} }; // polymorphic

struct Derived : Poly { int extra; };

int main() {
    Derived d;
    Poly* p = &d;

    // Correct: yields typeid(Derived) because Poly is polymorphic
    auto idx = dynamic_type_index(p);   // ok

    // Compile error: static_assert fires, catching the mistake immediately
    // dynamic_type_index(static_cast<NonPoly*>(nullptr)); // error
}

The static_assert / requires guard fires at the call site, not deep inside a dispatch table, producing a clear, actionable message. Without it, the code compiles and runs β€” it just dispatches to the wrong handler or produces silent no-ops, which can be extremely difficult to diagnose.

Best Practice: wrap typeid(*ptr) in a guarded helper like dynamic_type_index() rather than calling the operator directly. The one-line wrapper costs nothing at runtime and makes the polymorphism requirement explicit at every call site.

Common Pitfall: typeid on a value type (non-pointer, non-reference) always returns the static type, even if the type is polymorphic. Only typeid(*ptr) or typeid(ref) β€” where ptr/ref is of polymorphic type β€” trigger virtual dispatch. typeid(obj) where obj is declared as a base class by value will return the base's type_info, regardless of the actual object layout.

Performance benchmark context

std::type_index operations are consistently cheap, but their relative cost compared to dynamic_cast is often surprising to developers new to RTTI-based dispatch.

OperationComplexityTypical cost (modern x86)
typeid(expr)O(1)~1–3 ns (one pointer dereference to vtable metadata)
type_index::hash_code()O(1)~1 ns (returns a precomputed value stored in type_info)
std::hash<std::type_index>{}(idx)O(1)~1 ns (delegates to hash_code())
unordered_map::find(idx) (no collision)O(1) expected~5–15 ns (hash + one or two pointer chases)
dynamic_cast<Derived*>(base_ptr)O(depth)~20–200 ns (walks inheritance hierarchy at runtime)
std::visit on std::variantO(1)~2–5 ns (compiler-generated jump table, zero heap)

hash_code() is not computed lazily β€” major implementations (libstdc++, libc++, MSVC STL) store it as a field in type_info or derive it directly from the mangled-name pointer, making every call a single load instruction. The unordered_map lookup dominates in practice, not the hash computation itself.

dynamic_cast traverses the base-class graph recorded in the RTTI structures. For shallow hierarchies (one or two levels) the difference versus type_index dispatch is small; for deep or diamond hierarchies it grows proportionally. If a profile shows dynamic_cast as a hot spot, replacing it with a type_index dispatch table is a well-established optimization.

Best Practice: if you need to call the same dispatch table tens of thousands of times per frame (e.g., a game engine's component system), consider caching the std::type_index for each type in a static local variable. Each call to typeid(T) for a non-polymorphic, statically-known type is typically eliminated by the optimizer, but caching makes the intent explicit and protects against compiler regressions.

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