概要
- RustとC++のポリモーフィズムの違いを比較・解説
- 静的ディスパッチ(ジェネリクス/モノモルフィゼーション)と動的ディスパッチ(dyn Trait)の挙動
- ゼロサイズ型(ZST)やアドレスの扱いの哲学の違い
- Rustにおけるvtableの管理方法とC++との根本的な違い
- Vec<Box>による異種型コレクションの実現方法
RustとC++のポリモーフィズム比較
- C++では 仮想関数 によるランタイムポリモーフィズム、 CRTP によるコンパイル時ポリモーフィズムの2つの主要手法
- 仮想関数は vtable をオブジェクト内に保持し、自動的に仮想ディスパッチ
- CRTPはテンプレートによる静的ディスパッチ。可読性は落ちるが実行時コストゼロ
- Rustでは dyn Trait による動的ディスパッチ、 ジェネリクス(静的ディスパッチ) によるモノモルフィゼーション
dyn TraitはC++の仮想関数に近いが、 vtable管理の哲学が異なる
RustのジェネリクスはCRTPに近いが、 型制約を明示的に記述 する点が特徴
C++: 型制約は暗黙的(duck typing的)
Rust: 型制約は明示的(traitの実装宣言)
静的ディスパッチ(ジェネリクス/モノモルフィゼーション)
- Rustのtraitを用いた静的ディスパッチ例
- trait Draw { fn draw(&self) -> &str; }
- impl Draw for Circle, Square などで具象型に実装
- fn draw_shape<T: Draw>(shape: T) で呼び出し
- コンパイラが型ごとに draw_shape::, draw_shape:: を生成
- 静的ディスパッチは 実行時コストゼロ、型がコンパイル時に決定
ゼロサイズ型(Zero-Sized Types: ZST)
- C++では空のstructも1バイトサイズ保証。 アドレスによる識別
- Rustでは ゼロサイズ型 は本当にサイズ0。 所有権による識別
- &a, &bのアドレスはデバッグ時は異なるが、リリースビルドでは同じになる場合あり
- アドレスの一意性は保証されない
- 識別は所有権とバインディング名で行う (borrow checkerが管理)
- Rustの哲学: 「アドレス」ではなく「所有権」で値の一意性を管理
動的ディスパッチ(dyn Trait)
- &dyn Traitは ワイドポインタ(16バイト) :データポインタ+vtableポインタ
- vtableは 型×トレイトごとに1つだけ生成 され、オブジェクト自体にはvtable情報を持たせない
- std::mem::transmuteでポインタの中身を確認可能
- 同じ型同じTraitならvtableポインタは同一、データポインタはインスタンスごとに異なる
異種型コレクションの実現
- RustのVecは Tが同一型・同一サイズ である必要
- Vec<Box>で 異なる型をまとめて格納可能
- Boxはヒープ上に実体を確保し、ワイドポインタで管理
- C++のvector<Base*>と似ているが、 vtable管理の場所と哲学が異なる
- C++はオブジェクト内部にvtableポインタ
- Rustは 呼び出し側でvtableを選択し、オブジェクト自体は何も知らない
vtableの哲学的違い
- C++:クラス定義時点で仮想関数かどうか決定、すべてのインスタンスがvtableを持つ
- Rust: 呼び出し時点で静的/動的ディスパッチを選択、型自体はvtableの存在を意識しない
- Traitの組み合わせごとにvtableが生成 され、DuckがFly/Swim両方実装してもvtableは別
まとめとRust的思考法
- RustをC++の別構文と考えるのは 本質を見誤る原因
- 所有権・型安全・動的ディスパッチの選択の柔軟性 がRustの特徴
- vtableの管理、ZST、型制約の明示性など、 Rust独自の設計哲学 を理解することが重要
- 動的ディスパッチが必要となる場面 (異種型コレクション・実行時型不定など)を正しく認識することが、Rustマスターへの第一歩