🦀

C++erのRust入門3:トレイト

2023/05/05に公開
2

トレイト

C++だとコンセプトと純粋仮想関数しか持たないクラスみたいなもの。

型が実装する関数を強制することができる。

トレイトの定義方法

関連関数(static関数みたいなもの)や、メソッドの定義は以下のように行う。

トレイトが他のトレイトを実装する必要がある場合は、 : トレイト + トレイト の様に書く。

トレイトを実装した型自身を指す場合は Self を使う。foo_genのように、デフォルト実装を記述することも可能。

trait Foo : Default + Display {
  // selfを受け取るメソッド
  fn bar(&self) -> u32;
  // selfを受け取って更新するメソッド
  fn foobar(&mut self, x: u32) -> ();
  // 関連関数とデフォルト実装。 後でわかったけど、関連関数は定義しない方がいい。
  fn foo_gen() -> Self { Self::default() };
}

トレイトの実装方法

impl トレイト for 型 で実装する。

Foo: Default + Display と定義していても、 impl はそれぞれのトレイトごとに実装しないとだめ。

#[derive(Debug)]
struct Hoge { x: u32 }

impl Default for Hoge {
  fn default() -> Self { Self { x: 0 } }
}

impl Foo for Hoge {
  fn foobar(&mut self, x: u32) -> () { self.x = x; }
  fn bar(&self) -> u32 { self.x }
}

impl Display for Hoge {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    write!(f, "Hoge.x = {}", self.x)
  }
}

VSCode でトレイトを自動的に実装することができれば楽なんだけど、出来ないのかな。

一度 impl トレイト for 型 {} と書いて保存したら、💡のアイコンが出てくるので、Ctrl+. →Implements missing members で書いてない関数の実装はしてくれたけど……。

impl Foo for Hoge {} だけ書いたら、impl Defaultimpl Display も自動生成して欲しい。

トレイトが何を継承してるとか、手動で調べるの? スーパートレイトがさらにスーパートレイトを必要とかだったら、もうくっっそ面倒くさくない?

Rust 屋は普段どうしてるんだろ?

既存の型にトレイトを実装する

u32 や外部クレートで実装された型に新たなメソッドとかを生やそうとしてもエラーになるけど、トレイトなら実装できるらしい。

trait X {
  fn x(&self) -> u32;
}

impl X for u32 {
  fn x(&self) -> u32 { *self }
}

// 以下はエラー
impl u32 {
  fn y(&self) -> u32 { *self }
}

トレイトを返す

トレイトを実装した型を関数が返す方法は2つほどあるみたい。

// コンパイラが自動的に Hoo を返すと推定
fn piyo() -> impl Foo {
  Hoge::foo_gen()
}

// こっちを使ったほうが無難っぽそう
fn piyo2<T: Foo>() -> T {
  T::default()
}

なお impl Foo + Debug の様にトレイトを追加することもできる。

この関数の呼び出し方は以下のようになる。

let mut p = piyo();
// : Hoge が必要
let mut p2: Hoge = piyo2();

一見すると impl Traits の方が簡単だけど、たぶんジェネリクスを使ったほうが良さそう。

トレイトを引数に渡す

引数に渡す場合も返り値にする場合と大体同様。

fn puri(foo: &mut impl Foo) {
  foo.foobar(10);
  println!("puri:foo = {foo}.")
}

fn puri2<T:Foo>(foo: &mut T) {
  foo.foobar(10);
  println!("puri2:foo = {foo}.")
}

fn puri3(foo: &mut (impl Foo + Debug)) {
  foo.foobar(10);
  println!("puri3:foo = {:}.", foo)
}


fn puri4<T>(f1: &mut T, f2: &mut T)
where
  T: Foo + Debug,
{
  foo.foobar(10);
  println!("puri4:f1 = {:}, f2 = {}.", f1, f2)
}

明示的にトレイトの型が同じである(prui4)ことを明示したい場合を除いて、基本的には impl Traits が楽なんじゃないかと思う。

で、この puri4 を先程のpp2 で実行しようとしたところ、エラーになった。

pp2 は最終的には同じ型 Hoge なのだが、呼び出し時には型解決される前の型で引数の解析が行われる模様。

puri(&mut p, &mut p2);
   = note: expected mutable reference `&mut impl Foo`
              found mutable reference `&mut Hoge`

ふーん?じゃぁ、こうしたらどうなる?

fn piyo() -> impl Foo + Debug{
  Hoge::foo_gen()
}
fn piyopiyo() -> impl Foo + Debug{
  Hoge::foo_gen()
}

let mut x = piyo();
let mut y = piyopiyo();
puri4(&mut x, &mut y);

とやってみたら、はい、ちゃんとエラーになりました。

   = note: expected mutable reference `&mut impl Foo + Debug` (opaque type at <src/main.rs:37:14>)
              found mutable reference `&mut impl Foo + Debug` (opaque type at <src/main.rs:40:18>)
   = note: distinct uses of `impl Trait` result in different opaque types

というわけで、戻り値に関してはジェネリクスを使い、引数は impl Traits を使っておけばいいんじゃないかな。

トレイトオブジェクト

Rustの場合は impl Traits を返す場合でも、最終的にはトレイトを実装した具体的な型にコンパイラが変換する。

Javaの Interface の様な扱いをするには、トレイトオブジェクトを使う。トレイトオブジェクトのサイズは動的に変化するので、Box<dyn Traits>Rc<dyn Traits>Arc<dyn Traits> などのようにスマートポインタが必要ならしい。……トレイトオブジェクトって中身は、データへのポインタとvtable へのポインタでサイズ固定にできる気がするんだけどなぁ?よくわからん……。

Object Safety

で、試したんだけど、コンパイルエラーが出た。

fn piyo3() -> Box<dyn Foo> {
  Box::new(Hoge{ x: 0 })
}

エラーはFooが Object Safety ではないかららしい。
具体的にはメソッド以外の関数(Default::default()->SelfFoo::gen_foo()->Self)があるとだめらしい。他にはジェネリクスを使ったメソッド(当然だけどSelfを返すのも該当)とかもだめと。

うん、ジェネリクスがあるとだめってのは理解できるけどさ、staticメソッドは vtable から単純に外せばよくね?まじで。なんでvtableに入れなきゃいけないん?

vtableになんで入れたがるのかわからないけど、だったら以下なら行けるんぢゃね?と思って試してみたけどだめだった。

なんで?fooの呼び出しって関数ポインタあれば絶対確定するぢゃん。

trait Foo2 {
  fn foo() -> ();// だめ
}
fn piyo3() -> Box<dyn Foo2> {
  todo!()
}

とりあえず、関連関数はトレイトに含めめず、別なトレイトに切り出したほうが後でdyn Traits を使いたくなったときに困らなそうである。

ジェネリクスと関連型

トレイトでジェネリクスをしようと思うと、純粋にジェネリクスを使う場合と、関連型を使う場合がある。

trait Bar1<I> {
  fn get(&self) -> I;
  fn set(&mut self, value: I) -> ();
}

trait Bar2 {
  type I;
  fn get(&self) -> Self::I;
  fn set(&mut self, value: Self::I) -> ();
}

実装は以下のようになる。

impl Bar1<u32> for Hoge {
  fn get(&self) -> u32 { self.x }
  fn set(&mut self, value: u32) -> () { self.x = value; }
}

impl Bar2 for Hoge {
  type I = u32;
  fn get(&self) -> Self::I { self.x }
  fn set(&mut self, value: Self::I) -> () { self.x = value; }
}

引数として使う場合はこんな感じ。関連型のほうがIの定義がいらないので、簡単に書ける。

戻り値のT::I が不要ならimpl Bar2 でもよい。

fn bar1<I, T: Bar1<I>>(x: &T) -> I { x.get() }
fn bar2<T: Bar2>(x: &T) -> T::I { x.get() }

どちらを使うかは、トレイトを実装しうる型に対してIが複数取りうるかどうかで決める。

関連型を使った場合、実装は1つしか作ることは出来ないが、ジェネリクスではIの型に応じて複数の実装を定義できる。したがって、関連型の場合は実装型からIへの写像が単射で、ジェネリクスの場合は単射ではない。

今回、Hoge に設定できる Iu32 しかないので、関連型を使ったほうが良い。でも、set,getする対象が String やら u32 やら複数考えられる型を扱いうる場合はジェネリクスで定義したほうが良い。

ちなみに、 HogeString の実装をはやした場合、bar1の実行方法が bar1::<u32, Hoge>(&p2)の様に面倒くさくなる。これ、Hoge は省略できないのかな…?

impl Bar1<String> for Hoge {}

bar1::<u32, Hoge>(&p2);
let x:String = bar1(&p2); // x: String から::<String, Hoge> を推測してくれる。

Discussion

msakutamsakuta

Javaの Interface の様な扱いをするには、トレイトオブジェクトを使う。トレイトオブジェクトのサイズは動的に変化するので、Box<dyn Traits> 、Rc<dyn Traits>、Arc<dyn Traits> などのようにスマートポインタが必要ならしい。……トレイトオブジェクトって中身は、データへのポインタとvtable へのポインタでサイズ固定にできる気がするんだけどなぁ?よくわからん……。

トレイトオブジェクトを返すのにスマートポインタは別に必要ないですよ。ミニマルな例が結構長くなってしまいますが、下記は static オブジェクトへのトレイトオブジェクトを参照で返す例です。

struct Hello(i32);

trait HelloTrait {
    fn hello(&self);
}

impl HelloTrait for Hello {
    fn hello(&self) {
        println!("Hello {}", self.0);
    }
}

static HELLO: Hello = Hello(42);

fn get_static_trait_obj() -> &'static dyn HelloTrait {
    &HELLO
}

また、引数のライフタイムに拘束されていれば 'static でないスタック変数でも返せます。

fn get_trait_obj(hello: &Hello) -> &dyn HelloTrait {
    hello
}

ただ、 Rust には借用チェッカーがあるので、 Box, Rc, Arc といった所有権を持つスマートポインタを返すことのほうが圧倒的に多いのは事実です。正直私も上記のようなコードが役に立つ場面は思いつきません。

スマートポインタを使わないトレイトオブジェクトを見かけるほとんどのケースは関数の引数です。
引数の参照先は呼び出し先の関数よりも長生きすることは自明なので、ライフタイム拘束で悩む必要がないためです。
ポリモーフィックな型のオブジェクトの集合を渡すにはこのようなやり方がほぼ唯一の方法だと思います。

fn use_trait_objs(hello_guys: &[&dyn HelloTrait]) {
    for hello in hello_guys {
        hello.hello();
    }
}

型が一つに定まるなら、 impl Trait を使って Monomorphize した方が最適化が効きやすいので、 dyn Trait はあまり使わないと思います。
ここら辺も C++ の継承とテンプレートの関係と同じですね。

なお &dyn Trait はスマートポインタでこそありませんが、vftable へのポインタを含むファットポインタです。 std::mem::size_of() で見るとポインタ2個分です。 C++ は vftable へのポインタをオブジェクトに埋め込むので、細かいですがそこも違います。

ちなみに C++ でもポリモーフィックな型を返すときはサイズが固定であることは必要です。
無理やり継承先オブジェクトを継承元の型として返すことはできますが、 Slicing が起きてしまいます。
(参考: https://godbolt.org/z/95fr67v4Y)
これは Java の interface のようなポリモーフィズムを使いたいのであれば期待する動作とは異なるかと思います。
普通は unique_ptr やら shared_ptr やら生のポインタやらを返すことによってサイズを固定すると思いますが、ここら辺の事情は Rust と同じかと思います。
まぁ Java は…全てがスマートポインタみたいなものですので。

nodamushinodamushi

コメントありがとうございます。

やっぱりトレイトオブジェクト自体は固定サイズですよね。

どこのページでその説明を読んだのか履歴が多すぎて分からないのですが、トレイトオブジェクトは動的サイズだから Box でラップみたいなことが書いてあって❓となっていました。

単純に関数の戻りとして返す場合はヒープ等に確保してスマートポインタで返すということなら納得です。

トレイトオブジェクトの構造そのものが動的サイズではなくて、トレイトオブジェクトのポインタが指す先が動的なサイズだから、開放するためにスマートポインタが必要、開放が必要ないならスマートポインタ不要ということだったんですね。