std::type_index は std::type_info オブジェクトへのポインタをカプセル化し、比較を基盤となる before() メンバー関数に委譲することで、翻訳単位間の順序付けを実現しています。C++ ABI では、リンクエディタが翻訳単位間で同一型の型情報を単一の標準対象にマージすること(COMDAT セクションや弱いシンボルを使用)を義務付けており、または物理アドレスの違いにもかかわらず before() が一貫した全体的な順序付けを提供することを保証します。したがって、std::type_index はこの ABI によって保証された比較を単にラップし、比較の時点で型が完全である必要なく、operator< およびハッシュサポートを提供します。このメカニズムはランタイム型情報 (RTTI) が有効であることに完全に依存しており、コンパイラはリンクエディタが共有ライブラリの境界を越えて型の同一性を重複排除または調整するために必要な型メタデータを放出しなければなりません。
ゲームエンジンのプラグインアーキテクチャを設計する際に、コンポーネント型をファクトリ関数にマッピングする中央レジストリが必要でした。各プラグイン(共有ライブラリ)は、typeid(Component).name() をキーとして使用してそのコンポーネントを登録しました。しかし、クロスプラットフォームテスト中に、1つの共有ライブラリにロードされたプラグインが別の共有ライブラリに存在するコアエンジンによって登録されたファクトリを取得しようとしたとき、std::map のルックアップが時折失敗することが分かりました。根本的な原因は、type_info::name() によって返される文字列名がコンパイラ間で異なり(GCC 対 Clang)、type_info オブジェクトの直接ポインタ比較が失敗したからです。各共有ライブラリは同じ型の異なる静的インスタンスを含んでいました。
解決策 1: 手動文字列正規化
コンパイラ特有の API(例: abi::__cxa_demangle)を使用して type_info::name() 文字列をデマンガリングおよび正規化し、標準キーを作成することを考慮しました。このアプローチは、デバッグに適した人間が読める識別子を提供することを約束しました。
利点: 人間が読めるキーはログ記録やシリアル化を容易にします。
欠点: デマンガリングは高価であり、文字列比較は整数比較よりも遅く、形式は実装依存であり、将来のコンパイラのアップグレードによってレジストリが壊れるリスクがあります。
解決策 2: 仮想継承およびカスタム RTTI
すべてのコンポーネントが、手動で割り当てられた整数定数を返す仮想 GetTypeID() メソッドを提供する基底クラスから継承することを要求することを検討しました。
利点: 決定論的で高速な整数比較が可能で、コンパイラ RTTI に依存しません。
欠点: 手動での ID 割り当ては衝突を引き起こす可能性が高く、クラスの階層を変更する必要があり、制御できない外部型を扱うことができません。
解決策 3: std::type_index の採用
レジストリを std::map<std::type_index, FactoryFunc> を使用するようにリファクタリングし、std::type_index(typeid(T)) をキーとして利用しました。
利点: 標準は ABI 準拠の type_info 比較を通じて翻訳単位間での一貫した順序付けとハッシュを保証しており、手動の ID 管理を不要にし、typeid を使用する既存のコードとシームレスに統合できます。
欠点: RTTI を有効にする必要があり(バイナリサイズの増加)、type_index オブジェクトはネットワーク送信や永続ストレージのためにシリアル化できません。
我々は 解決策 3 を選択しました。なぜなら、クロスライブラリの型識別の信頼性が RTTI のバイナリサイズコストを上回ったからです。標準で定められた std::type_index の動作は、代替手段が抱えていた脆弱な文字列解析と手動の ID 管理を排除しました。
レジストリは Linux、Windows、macOS の DLL 境界を越えて正しく機能しました。ファクトリのルックアップは、文字列操作ではなく内部ポインタの O(log N) 比較に変わり、デマンガリングアプローチと比較してコンポーネントのインスタンス化のレイテンシを約 40% 削減しました。システムは、コアエンジンの型を再登録することなくプラグインのホットリロードをサポートするようになりました。
std::type_info::name() は実装依存のヌル終端バイト列を返します; C++ 標準はその形式、エンコーディング、安定性を明示的に定義することを拒否しています。例えば、GCC は通常、マングルされた名前(例: "St6vectorIiSaIiEE")を返す一方で、MSVC は人間が読める名前(例: "class std::vector<int,class std::allocator<int> >")を返します。コンパイラベンダーはデバッグの改善やシンボル長の短縮のために将来的にこれらの表現を変更することがあるため、これらの文字列をディスクやネットワークプロトコルにシリアル化すると、コンパイラのアップグレード時に未定義の動作を引き起こします。以前保存されたキーが新しく生成されたものと一致しなくなるからです。候補者はしばしば name() が安定した UUID のように振る舞うと誤解します。
RTTI が無効な場合、コンパイラは多態型型の type_info オブジェクトを放出せず、typeid 演算子が無効になります(静的型の表現を除き、これはいくつかの実装では静的型情報を返しますが、一般的には無効です)。std::type_index は構築のために const std::type_info& が必要ですが、RTTI がないため、必要な型メタデータはバイナリ内に存在しません。このため、生成されたメタデータに対するコンパイル時の依存性があるため、コンパイラはリンク時にエラー(例: "undefined reference to typeinfo for X")を出力し、捕捉可能な実行時例外に委ねることはありません。候補者はしばしば実行時の std::bad_typeid やそれに類似したものを期待し、dynamic_cast の失敗と混同します。
std::type_index は内部的に std::type_info オブジェクトへのポインタ(または参照)を格納します。C++20 およびそれ以前の非型テンプレートパラメータは、すべてのメンバーが公開されていて構造的な型である必要があり(またはそれらの配列)、動的ストレージまたはリンカー依存のアドレスを持つオブジェクトへのポインタを含めることはできません。type_info オブジェクトは静的ストレージに存在し、リンカー依存のアドレスを持つため、std::type_index は構造的型ではなく(いくつかの実装ではプライベートメンバーおよびトリビアルでないコピーコンストラクタを持ちます)、NTTP として使用することはできません。C++23 は定数式での typeid の使用を許可していますが、std::type_index 自体はほとんどの標準ライブラリ実装で非リテラルまたは非構造的であるため、コンパイル時定数が必要なテンプレート引数として使用することはできません。