🍢

【Rust】?Sized?

2024/02/12に公開1

Rustをやっていると、初見では何が何だかわからない、というものが度々出てきます。
そのうちの一つが

?Sized

というものです。この頭についている?は何なの?と、自分の頭にも?が溢れ出してきます。

今回は、この辺りを深堀していってみようと思います。

発端

私がこの件について調査したきっかけは、Traitの型パラメーターに&[u8]を使いたいけどジェネリクスでそれを実現する方法がわからなかったので、Rustのコミュニティに要望を投げたのがきっかけでした。

その当時のやり取りがこちらです。
https://internals.rust-lang.org/t/about-borrowing-used-only-in-trait-arguments/20257

struct StructA<A,B=A>{
    _marker: std::marker::PhantomData<fn() -> (A, B)>
}

struct StructB{
    a:StructA<isize,&[u8]>
}

trait TraitA<A,B> {
    fn convert_value(&mut self, input: B) -> A;
}

impl TraitA<isize,&[u8]> for StructB{
    fn convert_value(&mut self, input: &[u8]) -> isize{
        todo!("convert from input to isize value.");
    }
}

このような事をすると、StructB内の&[u8]にライフタイムの指定が無い、とBorrow checkerに叱られます。記述的には借用なので一見正当な主張に見えます。
しかし、StructAにはプロパティとして値を保持するわけではないので&[u8]の寿命はどうでもよく、convert_value(&mut self, input: &[u8])の実行中だけ生きておれば良いだけなので、最終的に構造体に保持されるかどうかまで追跡してから借用チェックするようにできませんか?という要望を出したわけです。

そうすると、

use std::marker::PhantomData;

struct StructA<A,B=A>
where B: ?Sized
{
    _marker: PhantomData<for<'a> fn(&'a B) -> A>
}

struct StructB{
    a: StructA<isize, [u8]>
}

こんな感じで書くと[u8]は渡せるよ、という回答をもらえました。
&[u8]ではないのですが、これはこれで一歩前進はしましたし、なによりRustを理解するきっかけにもなりました。

このコードの中に今回の表題である?Sizedが含まれていますが、もう一つ別に謎の記述があります。
for<'a>
という記述です。これについては最終的には今回の問題解決には使用しなかったため理解が浅く、今回の話題からは除外します。

Sizedとは何か?

?Sizedを理解するためには、まずはSizedというのが何かを知らなければなりません。
Sizedというのは、端的に言うとコンパイル時にサイズが決まる型の事です。
コンパイラがその事をどうやって検知するのかというと、Rustという言語は、型に関する制限をtrait境界というもので纏めて処理するのが流儀のようで、SizedというTraitが実装(impl)されている型はSizedな型である、と判断されます。
このように、特別な振る舞いがあるわけではないけど、コンパイルに関わる特別な性質を表すために存在するTraitをマーカートレイトと呼ぶようです。
他にも、Copy、Send、Syncなどがあります。

基本的には、structに保持するプロパティはSizedである必要があります。

struct内にSizedではない型が入ると、そのstruct内の全てのプロパティのオフセットが実行毎に変わる可能性が出てきます。そうなると、サイズが固定である事を前提にしたメモリアクセスができなくなるため、そのオフセットを保持するための余計な情報を入れる必要があり、もはや固定のオフセットでのアクセスが不可能なため、プロパティにアクセスするためのAPIが必要になったりと、複雑化、高負荷化、低速化する事が想像に難くありません。

そのため、ジェネリックな型を定義する際は、

struct StructA<X:Sized>{
    hoge:X
}

と、型パラメーターのtrait境界に暗黙的にSizedが指定されているのと同じような状態になっているようです。

今回直面した問題の本質

改めて問題のコードを持ってきます。

struct StructA<A,B=A>{
    _marker: std::marker::PhantomData<fn() -> (A, B)>
}

struct StructB{
    a:StructA<isize,&[u8]>
}

trait TraitA<A,B> {
    fn convert_value(&mut self, input: B) -> A;
}

impl TraitA<isize,&[u8]> for StructB{
    fn convert_value(&mut self, input: &[u8]) -> isize{
        todo!("convert from input to isize value.");
    }
}

問題は二段階になっています。
まず一つは
・structBで借用(&)の記述があるのにライフタイムパラメーターを指定していない事
です。
これについては、二つの解決のアプローチがあります。

1.ライフタイムパラメーターを指定する

struct StructB<'a>{
    a:StructA<isize,&'a [u8]>
}

ただし、この方法は、StructBをプロパティとして持つstructにライフタイムパラメーターを強制する事になり、結構な茨の道を歩むことになります。場合によっては別の問題を生む可能性が高く、今回は却下としました。

2.借用を使わない

struct StructB{
    a:StructA<isize,[u8]>
}

として、借用を使わずに通常の型として指定する事です。
しかし、この書き方の場合、今回の表題であるメインテーマであるtrait境界に引っ掛かり、別のエラーでコンパイルが通りません。

[u8]はSizedマーカートレイトが実装されてない型であり、コンパイル時にサイズが定まらない型なのです。

しかし、

struct StructA<A,B:?Sized=A>{
    _marker: std::marker::PhantomData<fn() -> (A, B)>
}
trait TraitA<A, B: ?Sized> {
    fn convert_value(&mut self, input: &B) -> A;
}

と、型パラメーターのtrait境界に?Sizeを指定してやることでコンパイルが通るようになります。

?Sized?

このように、trait境界に?Sizedと指定する事で、Sizedでは無い型も渡す事ができました。
この場合でも、渡された型の値をプロパティとして格納しようとするとエラーになります。
しかし、今回私が直面したケースのように、保存はしないけど関数の引数にSizedではない型を渡したいようなケースはありえます。そのような場合に限れば、今回示した通りの記述は有効な解決策になりえそうです。

では、そもそも、このマーカートレイトの前に?を付ける記法は何なのでしょうか?この記法に名前が付いているかどうかはわからないのですが、挙動としては、Sizedであるかもしれないし、Sizedではないかもしれない型として扱われるようです。

この挙動はその性質上、暗黙的に指定されるtrait境界にしか適用されないものと思われ、つまりマーカートレイトにのみ適用される可能性があるものと思われます。
SendやSyncにも適用される事があるのかを調べてみましたが、これらは暗黙的に指定されるケースはなさそうなので、現状では実質的にはSizedにのみ使用されるもののようです。知らんけど。

まとめ

trait境界の役割は、型が実装しているべきtraitを指定することにあります。
これらは、基本的には明示的に指定する必要がありますが、Sizedに関しては例外的に暗黙的に指定されているような感じになってます。これを明示的に指定から外す場合に?Sizedを付ける、というイメージが実際の挙動に近いと思われます。

余談:というか、エラーメッセージおかしくない?

ところで、冒頭に挙げたコードの&[u8]を[u8]にした場合

fn main(){
    struct StructA<A,B=A>{
        _marker: std::marker::PhantomData<fn() -> (A, B)>
    }
    
    struct StructB{
        a:StructA<isize,[u8]>
    }
}

エラーメッセージが

error[E0277]: the size for values of type `[u8]` cannot be known at compilation time
 --> src\main.rs:7:11
  |
7 |         a:StructA<isize,[u8]>
  |           ^^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
  |
  = help: the trait `Sized` is not implemented for `[u8]`
note: required by a bound in `StructA`
 --> src\main.rs:2:22
  |
2 |     struct StructA<A,B=A>{
  |                      ^^^ required by this bound in `StructA`
help: consider relaxing the implicit `Sized` restriction
  |
2 |     struct StructA<A,B=A: ?Sized>{
  |                         ++++++++

となります。

このエラーメッセージの通り修正すると、次は別のエラーになってコンパイルが通りません。

正しくは、

2 |     struct StructA<A,B: ?Sized=A>{
  |                       ++++++++

であるべきです。

おそらくずっと以前からあるバグだと思われますが、ともかく折角の機会なので、githubのissueに登録してみました。

https://github.com/rust-lang/rust/issues/120878

さらに、折角なので、自分で修正してプルリクエストまでしてみました。
※まだやりとりは続いていますが、順当にいけばマージされそうな雰囲気です。

https://github.com/rust-lang/rust/pull/120915

この辺りの詳細はまた改めて記事にしてみようと思います。

Discussion

白山風露白山風露

Sized のリファレンスに

All type parameters have an implicit bound of Sized. The special syntax ?Sized can be used to remove this bound if it’s not appropriate.

とあるように、 ?SizedSized 専用の記法ですね。そもそも、マーカートレイトであれそれ以外であれ、暗黙的に指定されるトレイト境界というものが Sized 以外にありません。普通のトレイト境界は明示しない場合、常に「実装されているかもしれないし実装されていないかもしれない」ものとして扱われるため、特に ?Sized のような指定をする必要がありません。

仮に新しく Sized と同じように暗黙的に指定されるトレイト X を導入することを考えると、

struct S<T:?Sized>;

のように任意の型を受け取るジェネリクスがあるとき、この SX 導入後では

struct S<T: ?Sized + X>;

と等価になり、

struct S<T: ?Sized + ?X>;

としなければ本来の目的を果たせなくなるため、大きな破壊的変更になってしまいます。
そのため、今後新しく同様のトレイトが追加されることはなく、 ?SizedSized 専用の構文のままでしょうね