トレイトオブジェクト

コードがポリモーフィズムを伴う場合、実際に実行するバージョンを決定するメカニズムが必要です。これは「ディスパッチ」(dispatch)と呼ばれます。ディスパッチには主に静的ディスパッチと動的ディスパッチという2つの形態があります。Rustは静的ディスパッチを支持している一方で、「トレイトオブジェクト」(trait objects)と呼ばれるメカニズムにより動的ディスパッチもサポートしています。

背景

本章の後のために、トレイトとその実装が幾つか必要です。単純に Foo としましょう。これは String 型の値を返す関数を1つ持っています。

fn main() { trait Foo { fn method(&self) -> String; } }
trait Foo {
    fn method(&self) -> String;
}

また、このトレイトを u8String に実装します。

fn main() { trait Foo { fn method(&self) -> String; } impl Foo for u8 { fn method(&self) -> String { format!("u8: {}", *self) } } impl Foo for String { fn method(&self) -> String { format!("string: {}", *self) } } }
impl Foo for u8 {
    fn method(&self) -> String { format!("u8: {}", *self) }
}

impl Foo for String {
    fn method(&self) -> String { format!("string: {}", *self) }
}

静的ディスパッチ

トレイト境界を使ってこのトレイトで静的ディスパッチが出来ます。

trait Foo { fn method(&self) -> String; } impl Foo for u8 { fn method(&self) -> String { format!("u8: {}", *self) } } impl Foo for String { fn method(&self) -> String { format!("string: {}", *self) } } fn do_something<T: Foo>(x: T) { x.method(); } fn main() { let x = 5u8; let y = "Hello".to_string(); do_something(x); do_something(y); }
fn do_something<T: Foo>(x: T) {
    x.method();
}

fn main() {
    let x = 5u8;
    let y = "Hello".to_string();

    do_something(x);
    do_something(y);
}

これはRustが u8String それぞれ専用の do_something() を作成し、それら特殊化された関数を宛てがうように呼び出しの部分を書き換えるという意味です。(訳注: 作成された専用の do_something() は「特殊化された関数」(specialized function)と呼ばれます)

trait Foo { fn method(&self) -> String; } impl Foo for u8 { fn method(&self) -> String { format!("u8: {}", *self) } } impl Foo for String { fn method(&self) -> String { format!("string: {}", *self) } } fn do_something_u8(x: u8) { x.method(); } fn do_something_string(x: String) { x.method(); } fn main() { let x = 5u8; let y = "Hello".to_string(); do_something_u8(x); do_something_string(y); }
fn do_something_u8(x: u8) {
    x.method();
}

fn do_something_string(x: String) {
    x.method();
}

fn main() {
    let x = 5u8;
    let y = "Hello".to_string();

    do_something_u8(x);
    do_something_string(y);
}

これは素晴らしい利点です。呼び出される関数はコンパイル時に分かっているため、静的ディスパッチは関数呼び出しをインライン化できます。インライン化は優れた最適化の鍵です。静的ディスパッチは高速ですが、バイナリ的には既にあるはずの同じ関数をそれぞれの型毎に幾つもコピーするため、トレードオフとして「コードの膨張」(code bloat)が発生してしまいます。

その上、コンパイラは完璧ではなく、「最適化」したコードが遅くなってしまうこともあります。 例えば、あまりにも熱心にインライン化された関数は命令キャッシュを膨張させてしまいます(地獄の沙汰もキャッシュ次第)。それが #[inline]#[inline(always)] を慎重に使うべきである理由の1つであり、時として動的ディスパッチが静的ディスパッチよりも効率的である1つの理由なのです。

しかしながら、一般的なケースでは静的ディスパッチを使用する方が効率的であり、また、動的ディスパッチを行う薄い静的ディスパッチラッパー関数を実装することは常に可能ですが、その逆はできません。これは静的な呼び出しの方が柔軟性に富むことを示唆しています。標準ライブラリはこの理由から可能な限り静的ディスパッチで実装するよう心がけています。

訳注: 「動的ディスパッチを行う薄い静的ディスパッチラッパー関数を実装することは常に可能だがその逆はできない」について

静的ディスパッチはコンパイル時に定まるのに対し、動的ディスパッチは実行時に結果が分かります。従って、動的ディスパッチが伴う処理を静的ディスパッチ関数でラッピングし、半静的なディスパッチとすることは常に可能(原文で「thin」と形容しているのはこのため)ですが、動的ディスパッチで遷移した値を元に静的ディスパッチを行うことはできないと言うわけです。

動的ディスパッチ

Rustは「トレイトオブジェクト」と呼ばれる機能によって動的ディスパッチを提供しています。トレイトオブジェクトは &FooBox<Foo> の様に記述され、指定されたトレイトを実装する あらゆる 型の値を保持する通常の値です。ただし、その正確な型は実行時になって初めて判明します。

トレイトオブジェクトはトレイトを実装した具体的な型を指すポインタから キャスト する(e.g. &x as &Foo )か、 型強制 する(e.g. &Foo を取る関数の引数として &x を用いる)ことで得られます。

これらトレイトオブジェクトの型強制とキャストは &mut T&mut Foo へ、 Box<T>Box<Foo> へ、というようにどちらもポインタに対する操作ですが、今の所はこれだけです。型強制とキャストは同一です。

この操作がまるでポインタのある型に関するコンパイラの記憶を「消去している」(erasing)ように見えることから、トレイトオブジェクトは時に「型消去」(type erasure)とも呼ばれます。

上記の例に立ち帰ると、キャストによるトレイトオブジェクトを用いた動的ディスパッチの実現にも同じトレイトが使用できます。

trait Foo { fn method(&self) -> String; } impl Foo for u8 { fn method(&self) -> String { format!("u8: {}", *self) } } impl Foo for String { fn method(&self) -> String { format!("string: {}", *self) } } fn do_something(x: &Foo) { x.method(); } fn main() { let x = 5u8; do_something(&x as &Foo); }

fn do_something(x: &Foo) {
    x.method();
}

fn main() {
    let x = 5u8;
    do_something(&x as &Foo);
}

または型強制によって、

trait Foo { fn method(&self) -> String; } impl Foo for u8 { fn method(&self) -> String { format!("u8: {}", *self) } } impl Foo for String { fn method(&self) -> String { format!("string: {}", *self) } } fn do_something(x: &Foo) { x.method(); } fn main() { let x = "Hello".to_string(); do_something(&x); }

fn do_something(x: &Foo) {
    x.method();
}

fn main() {
    let x = "Hello".to_string();
    do_something(&x);
}

トレイトオブジェクトを受け取った関数が Foo を実装した型ごとに特殊化されることはありません。関数は1つだけ生成され、多くの場合(とはいえ常にではありませんが)コードの膨張は少なく済みます。しかしながら、これは低速な仮想関数の呼び出しが必要となるため、実質的にインライン化とそれに関連する最適化の機会を阻害してしまいます。

何故ポインタなのか?

Rustはガーベジコレクタによって管理される多くの言語とは異なり、デフォルトではポインタの参照先に値を配置するようなことはしませんから、型によってサイズが違います。関数へ引数として渡されるような値を、スタック領域へムーブしたり保存のためヒープ領域上にメモリをアロケート(デアロケートも同様)するには、コンパイル時に値のサイズを知っていることが重要となります。

Foo のためには、 String (24 bytes)か u8 (1 byte)もしくは Foo (とにかくどんなサイズでも)を実装する依存クレート内の型のうちから少なくとも1つの値を格納する必要があります。ポインタ無しで値を保存した場合、その直後の動作が正しいかどうかを保証する方法がありません。型によって値のサイズが異なるからです。

ポインタの参照先に値を配置することはトレイトオブジェクトを渡す場合に値自体のサイズが無関係になり、ポインタのサイズのみになることを意味しています。

トレイトオブジェクトの内部表現

トレイトのメソッドはトレイトオブジェクト内にある伝統的に「vtable」(これはコンパイラによって作成、管理されます)と呼ばれる特別な関数ポインタのレコードを介して呼び出されます。

トレイトオブジェクトは単純ですが難解でもあります。核となる表現と設計は非常に率直ですが、複雑なエラーメッセージを吐いたり、予期せぬ振る舞いが見つかったりします。

単純な例として、トレイトオブジェクトの実行時の表現から見て行きましょう。 std::raw モジュールは複雑なビルドインの型と同じレイアウトの構造体を格納しており、 トレイトオブジェクトも含まれています

fn main() { mod foo { pub struct TraitObject { pub data: *mut (), pub vtable: *mut (), } } }
pub struct TraitObject {
    pub data: *mut (),
    pub vtable: *mut (),
}

つまり、 &Foo のようなトレイトオブジェクトは「data」ポインタと「vtable」ポインタから成るわけです。

dataポインタはトレイトオブジェクトが保存している(何らかの不明な型 T の)データを指しており、vtableポインタは T への Foo の実装に対応するvtable(「virtual method table」)を指しています。

vtableは本質的には関数ポインタの構造体で、実装内における各メソッドの具体的な機械語の命令列を指しています。 trait_object.method() のようなメソッド呼び出しを行うとvtableの中から適切なポインタを取り出し動的に呼び出しを行います。例えば、

fn main() { struct FooVtable { destructor: fn(*mut ()), size: usize, align: usize, method: fn(*const ()) -> String, } // u8: fn call_method_on_u8(x: *const ()) -> String { // // the compiler guarantees that this function is only called // // with `x` pointing to a u8 // コンパイラは `x` がu8を指しているときにのみこの関数が呼ばれることを保障します let byte: &u8 = unsafe { &*(x as *const u8) }; byte.method() } static Foo_for_u8_vtable: FooVtable = FooVtable { // destructor: /* compiler magic */, destructor: /* コンパイラマジック */, size: 1, align: 1, // // cast to a function pointer // 関数ポインタへキャスト method: call_method_on_u8 as fn(*const ()) -> String, }; // String: fn call_method_on_String(x: *const ()) -> String { // // the compiler guarantees that this function is only called // // with `x` pointing to a String // コンパイラは `x` がStringを指しているときにのみこの関数が呼ばれることを保障します let string: &String = unsafe { &*(x as *const String) }; string.method() } static Foo_for_String_vtable: FooVtable = FooVtable { // destructor: /* compiler magic */, destructor: /* コンパイラマジック */, // // values for a 64-bit computer, halve them for 32-bit ones // この値は64bitコンピュータ向けのものです、32bitコンピュータではこの半分にします size: 24, align: 8, method: call_method_on_String as fn(*const ()) -> String, }; }
struct FooVtable {
    destructor: fn(*mut ()),
    size: usize,
    align: usize,
    method: fn(*const ()) -> String,
}

// u8:

fn call_method_on_u8(x: *const ()) -> String {
    // コンパイラは `x` がu8を指しているときにのみこの関数が呼ばれることを保障します
    let byte: &u8 = unsafe { &*(x as *const u8) };

    byte.method()
}

static Foo_for_u8_vtable: FooVtable = FooVtable {
    destructor: /* コンパイラマジック */,
    size: 1,
    align: 1,

    // 関数ポインタへキャスト
    method: call_method_on_u8 as fn(*const ()) -> String,
};


// String:

fn call_method_on_String(x: *const ()) -> String {
    // コンパイラは `x` がStringを指しているときにのみこの関数が呼ばれることを保障します
    let string: &String = unsafe { &*(x as *const String) };

    string.method()
}

static Foo_for_String_vtable: FooVtable = FooVtable {
    destructor: /* コンパイラマジック */,
    // この値は64bitコンピュータ向けのものです、32bitコンピュータではこの半分にします
    size: 24,
    align: 8,

    method: call_method_on_String as fn(*const ()) -> String,
};

各vtableの destructor フィールドはvtableが対応する型のリソースを片付ける関数を指しています。 u8 のvtableは単純な型なので何もしませんが、 String のvtableはメモリを解放します。このフィールドは Box<Foo> のような自作トレイトオブジェクトのために必要であり、 Box によるアロケートは勿論のことスコープ外に出た際に内部の型のリソースを片付けるのにも必要です。 size 及び align フィールドは消去された型のサイズとアライメント要件を保存しています。これらの情報はデストラクタにも組み込まれているため現時点では基本的に使われていませんが、将来、トレイトオブジェクトがより柔軟になることで使われるようになるでしょう。

例えば Foo を実装する値を幾つか得たとします。 Foo トレイトオブジェクトを作る、あるいは使う時のコードを明示的に書いたものは少しだけ似ているでしょう。(型の違いを無視すればですが。どのみちただのポインタになります)

fn main() { let a: String = "foo".to_string(); let x: u8 = 1; // let b: &Foo = &a; let b = TraitObject { // // store the data // データを保存 data: &a, // // store the methods // メソッドを保存 vtable: &Foo_for_String_vtable }; // let y: &Foo = x; let y = TraitObject { // // store the data // データを保存 data: &x, // // store the methods // メソッドを保存 vtable: &Foo_for_u8_vtable }; // b.method(); (b.vtable.method)(b.data); // y.method(); (y.vtable.method)(y.data); }
let a: String = "foo".to_string();
let x: u8 = 1;

// let b: &Foo = &a;
let b = TraitObject {
    // データを保存
    data: &a,
    // メソッドを保存
    vtable: &Foo_for_String_vtable
};

// let y: &Foo = x;
let y = TraitObject {
    // データを保存
    data: &x,
    // メソッドを保存
    vtable: &Foo_for_u8_vtable
};

// b.method();
(b.vtable.method)(b.data);

// y.method();
(y.vtable.method)(y.data);

オブジェクトの安全性

全てのトレイトがトレイトオブジェクトとして使えるわけではありません。例えば、ベクタは Clone を実装していますが、トレイトオブジェクトを作ろうとすると、

fn main() { let v = vec![1, 2, 3]; let o = &v as &Clone; }
let v = vec![1, 2, 3];
let o = &v as &Clone;

エラーが発生します。

error: cannot convert to a trait object because trait `core::clone::Clone` is not object-safe [E0038]
let o = &v as &Clone;
        ^~
note: the trait cannot require that `Self : Sized`
let o = &v as &Clone;
        ^~

エラーは Clone が「オブジェクト安全」(object-safe)でないと言っています。トレイトオブジェクトにできるのはオブジェクト安全なトレイトのみです。以下の両方が真であるならばトレイトはオブジェクト安全であるといえます。

では何がメソッドをオブジェクト安全にするのでしょう?各メソッドは Self: Sized を要求するか、以下の全てを満足しなければなりません。

ひゃー!見ての通り、これらルールのほとんどは Self について話しています。「特別な状況を除いて、トレイトのメソッドで Self を使うとオブジェクト安全ではなくなる」と考えるのが良いでしょう。