Using std::type_index as Map Keys in C++

In modern C++, std::type_index serves as a wrapper around std::type_info that allows runtime type information to be used as keys in associative containers like std::map and std::unordered_map. While the typeid operator returns a reference to std::type_info, that object cannot be used directly in standard containers because it is non-copyable and lacks standard hashing support. This article explains the limitations of std::type_info, how std::type_index resolves these issues, and how to implement type-indexed associative maps efficiently.

The Limitation of std::type_info

When you query type information at runtime using typeid(T), C++ returns a const reference to a std::type_info object. However, std::type_info cannot be stored directly as a key in a map due to specific language design constraints:

  • Non-Copyable and Non-Assignable: The copy constructor and copy assignment operator of std::type_info are deleted, preventing map containers from storing value copies.
  • No Native Strict Weak Ordering: Standard std::type_info only provides .before(), which cannot be used directly by std::less without custom comparators.
  • No Hash Support: There is no default std::hash<std::type_info> specialization, preventing its direct use in std::unordered_map.

How std::type_index Solves the Problem

Introduced in C++11 and defined in the <typeindex> header, std::type_index is a lightweight, copyable wrapper around a pointer to const std::type_info. It bridges the gap between runtime type identification and the container requirements by providing:

  1. Copyability and Assignability: Instances can be freely copied, moved, and assigned inside containers.
  2. Comparison Operators: It overloads operator<, operator<=, operator>, operator>=, operator==, and operator!=. The relational operators use the underlying std::type_info::before() method, satisfying the strict weak ordering requirement for ordered containers like std::map.
  3. Hash Specialization: The standard library provides a specialization for std::hash<std::type_index>, which delegates to std::type_info::hash_code(). This enables out-of-the-box support for std::unordered_map.

Implementation Example

The following example demonstrates using std::type_index with both std::map and std::unordered_map to map types to string descriptions or factory functions:

#include <iostream>
#include <typeindex>
#include <unordered_map>
#include <string>
#include <memory>

class Base { public: virtual ~Base() = default; };
class DerivedA : public Base {};
class DerivedB : public Base {};

int main() {
    // std::unordered_map using std::type_index as the key
    std::unordered_map<std::type_index, std::string> typeNames;

    // Populating the map using typeid wrapped in std::type_index
    typeNames[std::type_index(typeid(int))] = "Integer";
    typeNames[std::type_index(typeid(double))] = "Double";
    typeNames[std::type_index(typeid(DerivedA))] = "DerivedA Class";

    // Querying with static types
    std::cout << "int maps to: " << typeNames[std::type_index(typeid(int))] << '\n';

    // Querying with dynamic polymorphic types
    std::unique_ptr<Base> instance = std::make_unique<DerivedA>();
    std::type_index runtimeType = std::type_index(typeid(*instance));

    if (typeNames.find(runtimeType) != typeNames.end()) {
        std::cout << "Dynamic instance type: " << typeNames[runtimeType] << '\n';
    }

    return 0;
}

Common Use Cases

  • Service Locators and Dependency Injection: Registering singletons or providers by their interface or concrete type.
  • Heterogeneous Containers: Building storage structures capable of holding arbitrary types alongside their type metadata without manual enum tags.
  • Event Dispatchers: Routing dynamic events to specific handler functions based on the dynamic event object's runtime type.