このページの目次

Trait オブジェクトと動的ディスパッチ

ジェネリクスはコンパイル時に各型ごとにコードを生成する(静的ディスパッチ、ランタイムオーバーヘッドゼロ)、dyn Trait は vtable を介して実行時にファンポインタを参照する(動的ディスパッチ、1つのコードで複数の型に対応)。オブジェクト安全性は、どのトレイトがトレイトオブジェクト化できるかを決定し、Box<dyn> vs enum は動的多態性と代数的多態性の選択である。

2つのディスパッチ: コンパイル時 vs 実行時

// 静的ディスパッチ: ジェネリクス。コンパイル時に各 T ごとに独立したコードを生成し、ランタイムオーバーヘッドなし
fn static_dispatch<T: Display>(x: &T) { println!("{}", x); }

// 動的ディスパッチ: trait オブジェクト。ポインタと vtable を介して、間接呼び出しのオーバーヘッドがある
fn dynamic_dispatch(x: &dyn Display) { println!("{}", x); }

どちらを使うべきか?コンパイル時にすべての可能な型が分かっている場合 → 静的ディスパッチ。型が実行時に決定される場合、または異種コレクション⁠(異なる具体的な型を同じコンテナに格納)が必要 → 動的ディスパッチ。古典的な異種シナリオ:

let items: Vec<Box<dyn Display>> = vec![
    Box::new(42_i32),
    Box::new(String::from("hello")),
    Box::new(3.14_f64),
];
for item in &items { println!("{}", item); }

vtable の内部実装

&dyn Display は実行時に2つのポインタ(ファットポインタ、64ビット環境で16バイト)として表現されます:

ptr_to_data:   ヒープ上の実際のデータを指す (例: i32 または String)
ptr_to_vtable: 静的な vtable 構造体を指す

i32 の Display としての vtable:
  [0]: drop_in_place<i32>    — 解放方法
  [1]: size_of::<i32> (4)    — データサイズ
  [2]: align_of::<i32> (4)   — アラインメント
  [3]: <i32 as Display>::fmt — 具体的な Display::fmt 関数ポインタ

x.fmt(f) を呼び出す際:vtable を読み取り → 3番目のエントリを取得 → 間接ジャンプ。静的呼び出しよりも1回分のポインタ間接参照が増える(通常、CPUの分岐予測器によってプリフェッチ可能だが、依然として追加の命令となる)。

オブジェクト安全性: 全てのトレイトが dyn になれるわけではない

トレイトオブジェクトは「オブジェクト安全性」を満たす必要がある。なぜなら、コンパイラは vtable を生成しなければならないからである。オブジェクト安全性を満たさないトレイトの例:

trait NotObjectSafe {
    fn clone(&self) -> Self;         // Self を返す — Self が何か分からない
    fn generic<T>(&self, x: T);      // ジェネリックメソッド — モノモーフィズが必要
}

// let x: &dyn NotObjectSafe = ...;  // エラー: このトレイトはオブジェクト安全ではない

Clone はオブジェクト安全ではない。fn clone(&self) -> Self の返却型は、コンパイル時に呼び出し元で知られていなければならないからである。したがって、Box<dyn Clone> は作成できない。ただし、Box<dyn Any> を使用して型消去とダウンキャストを行うことは可能である。

dyn と enum の使い分け

// enum: コンパイル時の閉集合、仮想ディスパッチなし
enum Shape { Circle(f64), Rectangle(f64, f64) }
impl Shape {
    fn area(&self) -> f64 {
        match self { Shape::Circle(r) => PI * r * r, Shape::Rectangle(w, h) => w * h }
    }
}

// dyn: 開集合(拡張可能)、仮想ディスパッチ(呼び出しごとに間接ジャンプを1回実行)
trait Shape { fn area(&self) -> f64; }
let shapes: Vec<Box<dyn Shape>> = vec![...];

enum は「すべての可能なバリアントを把握しており」、バリアントの数が少なく、頻繁に変更されない場合に適している。dyn は「外部がさらに多くの実装を追加できる」場合や、バリアントが多すぎる場合(数十〜数百の enum バリアントに拡張するのは現実的でない)に適している。

参考

  • Rust Book: 第17章 17.2節
  • Rust Reference: vtable のレイアウト、オブジェクト安全性
  • Rustonomicon: dyn Trait の内部構造

Keywords: dyn Trait, vtable, object safety, Box, dynamic dispatch, static dispatch, fat pointer