`Sized` トレイトと `?Sized`

Essential
最終更新: タグ: Rust, メモリ

Rust が値をスタックに置いたり、関数に値渡ししたり、別の型の中に格納したりする前に、何バイト確保するかを知る必要がある。Sized は「この型はコンパイル時にサイズが判明している」という形式的なマーカーだ。自分で実装する必要はない — コンパイラがサイズの固定したすべての型に自動的に付与する。

Sized とは何か

Sized はメソッドを持たない自動実装の**マーカートレイト(marker trait)**だ:

// 標準ライブラリで定義されている — メソッドもフィールドもない
pub marker trait Sized {}

コンパイル時にサイズが判明しているすべての具体型は自動的に Sized を実装する。型のサイズで見た Sized でない型は動的サイズ型(DST: dynamically sized type)と呼ばれる:[T]strdyn Trait だ。

暗黙的な T: Sized 境界

すべてのジェネリック型パラメータは隠れた Sized 境界を持つ。次の二つのシグネチャは同一だ:

fn identity<T>(x: T) -> T { x }
fn identity<T: Sized>(x: T) -> T { x } // コンパイラが実際に見るもの

この境界が存在するのは、x が値渡しされるからだ。コンパイラはスタック上に何バイト確保するかを知らなければならない。[u8] はコンパイル時のサイズを持たないため、identity[u8] 引数で呼ぶことはできない。

?Sized による境界の緩和

?Sized はトレイトではない — 暗黙の Sized 要求を取り除く境界だ。サイズのある型もない型も受け取るには T: ?Sized と書く:

fn print_it<T: ?Sized + std::fmt::Debug>(x: &T) {
    println!("{:?}", x);
}

print_it(&42u32);        // サイズあり:OK
print_it(&[1u8, 2, 3]);  // サイズなしスライス:これも OK

サイズのない T はスタックに置けないため、T: ?Sized を受け取る関数は参照またはポインタ越しにのみ T を使う。その参照はファットポインタ(fat pointer)だ — 二つのポインタ幅を持つ:データアドレス用と、メタデータ(スライスなら長さ、トレイトオブジェクトなら vtable ポインタ)用だ。

標準ライブラリにおける ?Sized

標準ライブラリは、ラッパー型が DST を保持できるようにしたいすべての箇所で ?Sized を使っている:

impl<T: ?Sized> Box<T> { ... }
impl<T: ?Sized> Rc<T>  { ... }
impl<T: ?Sized> Arc<T> { ... }

?Sized がなければ、Box<dyn Trait>Box<[u8]> はコンパイルエラーになる。T に対する Sized 要求を外すことで、これらの型はサイズあり・なし両方のポインティーに対してジェネリックになる。

str[T]

最もよく見かける DST は str[T] だ。直接保持することはほとんどなく、参照またはスマートポインタ越しにのみ使う:

let s: &str = "hello";       // ファットポインタ:ptr + 長さ
let v: &[i32] = &[1, 2, 3];  // ファットポインタ:ptr + 長さ

StringVec<T> はヒープバッファを所有する Sized 型で、それぞれ str[T] に逆参照される。

まとめ

  • Sized はコンパイル時にサイズが判明しているすべての型に自動実装される。
  • すべてのジェネリックパラメータは暗黙的に T: Sized を要求し、値をスタック上に値渡しできるようにする。
  • T: ?Sized はその要求を取り除く。関数は参照またはポインタ越しにのみ T を使わなければならない。
  • DST — [T]strdyn Trait — は Sized でなく、それらへの参照はファットポインタだ。
  • Box<T>Rc<T>Arc<T>T: ?Sized を使い、サイズあり・なし両方の型をラップできる。