C++erのRust入門3:トレイト
トレイト
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 Default
と impl 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
を先程のp
と p2
で実行しようとしたところ、エラーになった。
p
と p2
は最終的には同じ型 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()->Self
と Foo::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
に設定できる I
は u32
しかないので、関連型を使ったほうが良い。でも、set,getする対象が String
やら u32
やら複数考えられる型を扱いうる場合はジェネリクスで定義したほうが良い。
ちなみに、 Hoge
に String
の実装をはやした場合、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
トレイトオブジェクトを返すのにスマートポインタは別に必要ないですよ。ミニマルな例が結構長くなってしまいますが、下記は
static
オブジェクトへのトレイトオブジェクトを参照で返す例です。また、引数のライフタイムに拘束されていれば
'static
でないスタック変数でも返せます。ただ、 Rust には借用チェッカーがあるので、
Box
,Rc
,Arc
といった所有権を持つスマートポインタを返すことのほうが圧倒的に多いのは事実です。正直私も上記のようなコードが役に立つ場面は思いつきません。スマートポインタを使わないトレイトオブジェクトを見かけるほとんどのケースは関数の引数です。
引数の参照先は呼び出し先の関数よりも長生きすることは自明なので、ライフタイム拘束で悩む必要がないためです。
ポリモーフィックな型のオブジェクトの集合を渡すにはこのようなやり方がほぼ唯一の方法だと思います。
型が一つに定まるなら、
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 は…全てがスマートポインタみたいなものですので。
コメントありがとうございます。
やっぱりトレイトオブジェクト自体は固定サイズですよね。
どこのページでその説明を読んだのか履歴が多すぎて分からないのですが、トレイトオブジェクトは動的サイズだから Box でラップみたいなことが書いてあって❓となっていました。
単純に関数の戻りとして返す場合はヒープ等に確保してスマートポインタで返すということなら納得です。
トレイトオブジェクトの構造そのものが動的サイズではなくて、トレイトオブジェクトのポインタが指す先が動的なサイズだから、開放するためにスマートポインタが必要、開放が必要ないならスマートポインタ不要ということだったんですね。