Trait objectのDynamic dispatch

  • trait objectってのは dyn Trait ←こういうの

なんでDynamic Dispatchが必要なの?

stdドキュメント より:

Unlike generic parameters or impl Trait, the compiler does not know the concrete type that is being passed. That is, the type has been erased. As such, a dyn Trait reference contains two pointers. One pointer goes to the data (e.g., an instance of a struct). Another pointer goes to a map of method call names to function pointers (known as a virtual method table or vtable).

Trait objectはGenericsと逆のことをしている。

  • Trait objectは
    • Traitに必要な関数へのポインタのみを型から抜き出してくる(汎化する)
    • その背後にある型は無視する・忘れる(型消去)
  • Genericsは、
    • 実際にTに入れられた型のそれぞれについて実装を作成する(特化する)
      • それぞれについて最適化ができる
      • 呼び出しは(コンパイル時に)そのコピーに仕向ける
    • 当然それぞれの実装がどの型に対応しているか、コンパイルの間は覚えていないといけない

Trait objectについては、もちろんコンパイル時にはその型がちゃんとTraitの実装を持っているか見るのだが、メモリレイアウト上では差をつくらない(同じ形のvtableを持つ)。

The compiler doesn’t know all the types that might be used with the code that is using trait objects

↑そんなことはなくね?

図解:

Vec<T>にはboundがついていない(T: ?Sizedもない)ので、デフォルトでT: Sizedを要求されることに注意。

型消去

また、型を「忘れる」のは意図的であることに注意する。

  • Dynamic dispatchingはstaticに比べてコードが小さくなるという利点がある。あえてdynamic dispatchingを採用しているときに、型の情報を覚えていたってしょうがないので忘れている、ということ。
  • Hoge へのポインタ、つまり Hoge の先頭のアドレスを持ってはいるが、その後にどんなデータが入っているか dyn は知らない

型消去は「あえて」型を無視させるので、他の言語ではまわりくどいコードを書かざるを得ないこともあるが、Rustではそれが dyn 一発でできる。

Object Safety

さて、そうやってもとの型を忘れてしまうと困るケースがある。たとえば、図の c() の引数や返り値が Self のとき。本来なら Hoge を返せばいいのだが、もう忘れてしまった。どうしようもないので、こういう関数はDynamic dispatchingの対象から外している(object safety)。

  • メソッドがgenericsを使っている場合も困る。Genericsを使うとコンパイル時に実装がコピーされる。c::<i64>::(), c::<u32>::(), c::<usize>::(), ...
    • わかんない
    • 別にvtableに全パターンぶち込んで呼べばよくね?
  • where Self: Sized をつける理由
    • Sizedだとobject-safeにならない?
      • Sizedは「コンパイル時にサイズがわかる」だけではなくて、「サイズが一定である」こともいう
      • traitはもとの型によってサイズがバラバラになるので、それをdynにしたtrait objectもDST(つまり!Sized)にならざるを得ない
      • Sizedにすると、(traitの場合)「このtraitはtrait objectになれませんよ」(メソッドの場合)「このメソッドはtrait objectでは使えませんよ」と宣言することと同義になる
    • 引数に self があるとき、そのselfSelf: Sizedを満たさなければならない一方で、元の型がわからないからコンパイル時にサイズがわからない

等価なコードをtrait objectとgenerics両方で書ける?

trait Foo {
    fn c();
    fn d();
}

impl Foo for Hoge {
    fn c() { do_something(); }
    fn d() { do_something_once_again(); }
}

impl Foo for Piyo {
    fn c() { do_something(); }
    fn d() { do_something_once_again(); }
}

let v: Vec<Box<dyn Foo>> = vec![Box::new(Hoge::new()), Box::new(Piyo::new())];
for vv in v { vv.c(); }

trait objectを使わないのなら…


hoge.c();
piyo.c(); 

と個別に呼ぶか、

fn c<F>(this: F) {
    // Foo.c()に相当する処理 
}

c(piyo);
c(hoge);

って感じにする?

Backlinks

There are no notes linking to this note.