FABRKNT
スタックを読む — 中級への橋渡し
ソース読みのための Rust
レッスン 6 / 10·CONTENT15 分30 XP
コース
スタックを読む — 中級への橋渡し
レッスンの役割
CONTENT
順序
6 / 10

レッスン5 — Generics・trait bounds・?Sized・dyn vs impl

問い

Reth / Revm / Alloy のソースで頻出する ジェネリクス + trait bounds<T: Bound> / ?Sized / dyn Trait / impl Trait — それぞれ何を意味し、なぜ使い分けるか。コードを読むのに必須の 4 概念

原理(最小モデル)

  • ジェネリクス <T>. 型パラメータ、コンパイル時に具象化(モノモーフ化)= 実行時オーバーヘッドなし + 各具象型用バイナリ生成。
  • Trait bounds T: Trait. 型パラメータに制約、fn print<T: Display>(x: T) で「Display 実装してれば何でも」、複数 bounds は T: A + B + \'static
  • ?Sized. デフォルトの Sized 制約を外す、dyn Trait(実行時サイズ)を受け入れる、<T: ?Sized> で trait object を渡せる。
  • dyn Trait. 実行時ディスパッチ、vtable 経由、Box<dyn Trait> / &dyn Trait、複数具象型を統一して扱う、サイズ未知。
  • impl Trait. 引数 = 「Trait 実装の何か」(ジェネリクスの短縮)、戻り値 = 「具体的な型を隠蔽」、戻り値の場合は単一具象型に限定。
  • ジェネリクス vs dyn. ジェネリクス = 静的ディスパッチ + モノモーフ化(速い、コード膨張)、dyn = 動的ディスパッチ + vtable(柔軟、若干遅い)。
  • Send + Sync. スレッド間移動 / 共有可能性のマーカー trait、Tokio タスクには T: Send + \'static 必要、Rust 並行性の土台。
  • \'static ライフタイム. プログラム全体生存 or 借用なし、グローバル定数 / String::from("...") / Vec<T> 等、Send + \'static でタスク移動可能。

具体例 + ステップで組み立てる

Generics・trait bounds・?Sized・dyn vs impl

これは pub fn add<IT: ITy, H: ?Sized>(...) を怯まず読めるようにするためのレッスンである。Reth / Revm のソースは generics が密集 し、関数シグネチャに型パラメータ 3 つは珍しくない。本レッスンではその仕組みを 1 つずつ解きほぐす。

Generics 101 — 基本の形

ジェネリック関数は 型パラメータ を山括弧で取り、それを通常の型のように使う:

fn first<T>(items: &[T]) -> Option<&T> {
    items.first()
}

let nums = [1, 2, 3];
let first_num = first(&nums);          // T は i32 と推論

let words = ["hello", "world"];
let first_word = first(&words);        // T は &str と推論

コンパイラは モノモーフ化 する — 各具象 T 用に first の特殊化コピーを生成する。first::<i32>first::<&str> はコンパイル後のバイナリでは別関数で、両方とも手書きの非ジェネリックコードと比べて実行時コストゼロ。

Trait bounds — 「T はこれらの操作をサポートする必要がある」

裸の <T> は「T は何でも良い」と言う。多くの場合 T で 何かする 必要がある — メソッド呼び出し、比較、フォーマット。そこで trait bounds が登場:

use std::fmt::Display;

fn print_first<T: Display>(items: &[T]) {
    if let Some(first) = items.first() {
        println!("{}", first);          // T: Display が要る
    }
}

<T: Display> は「T は Display トレイトを実装している必要がある」と読む。これがないとコンパイラは拒否 — T が {} フォーマッタを持つか知らない。

+ で複数 bound:

fn process<T: Display + Clone>(item: T) {
    let copy = item.clone();
    println!("{} (cloned: {})", item, copy);
}

where 節 — 同じこと、別構文

bound が長くなるとシグネチャの下に移動:

fn process<T>(item: T)
where
    T: Display + Clone + Send + 'static,
{
    // 本体
}

これは純粋に表記の話。<T: Bound>where T: Bound は完全に同じ意味。Reth コードは長さに応じて両方使う。

Sized — あなたが知らなかった暗黙 bound

Rust のすべての型パラメータ T には 暗黙の Sized bound がある。つまり <T> は静かに <T: Sized> になっている。Sized は「コンパイラがコンパイル時に型のサイズを知っている」という意味。

ほとんどの型は Sized: i32 は 4 バイト、String は 24 バイト (ポインタ + 長さ + 容量)、自作の struct は既知のサイズ。

一部の型は Sized でない

  • str (裸の文字列型、&str ではない) — 長さは中身次第
  • [i32] (裸のスライス型、&[i32] ではない) — 同様
  • dyn Trait (これから扱う) — 裏の具象型は何でもありうる

<T: ?Sized> と書くと、暗黙の Sized bound から opt out している。T は unsized でも良くなる。? は「Sized かもしれない、そうでないかもしれない」。

なぜそうするか? &TBox<T> は T が ?Sized なら unsized 型を保持できるから。?Sized がないと fn foo<T>(x: &T) を書いて &dyn Trait を渡すことは絶対にできない — コンパイラは T: Sized を強制し、dyn Trait はそれを満たさない。

Revm がこう書く時:

pub fn add<IT: ITy, H: ?Sized>(context: Ictx<'_, H, IT>) -> Result {

H: ?Sizedまさに Hdyn Host になれるため。関数は &MyConcreteHost (Sized) と &mut dyn Host (unsized、?Sized) の両方で動く。

この 1 文字 (?) が、関数がトレイトオブジェクトを受け入れるかを決める

dyn Trait — トレイトオブジェクト

dyn Traitトレイトオブジェクト。「ポインタ + vtable」のペア:

  • ポインタが具象値を指す
  • vtable は関数ポインタの表 — トレイトの各メソッドに 1 つ

dyn Traitobj.method() を呼ぶと、コンパイラは vtable ルックアップ を吐く: vtable からメソッドポインタをロード、間接呼び出し。これが 動的ディスパッチ — 実行時に解決される。

dyn Trait 自体は unsized (ポインタの裏の具象型はどんなサイズでもありうる)。だから常に何らかのポインタ越しに見る:

&dyn Trait        // 共有参照
&mut dyn Trait    // 排他参照
Box<dyn Trait>    // 所有、ヒープ確保
Rc<dyn Trait>     // 共有所有、シングルスレッド
Arc<dyn Trait>    // 共有所有、スレッドセーフ

混在実装のベクタ:

trait Greet { fn greet(&self); }
struct En; struct Ja;
impl Greet for En { fn greet(&self) { println!("Hi"); } }
impl Greet for Ja { fn greet(&self) { println!("こんにちは"); } }

let mixed: Vec<Box<dyn Greet>> = vec![Box::new(En), Box::new(Ja)];
for g in &mixed { g.greet(); }

dyn がないと不可能 — Vec<T> は全要素が同じ具象型である必要がある。

impl Trait — 静的な対応物

impl Trait は似て見えるが根本的に違う:

fn make_greeter(lang: &str) -> impl Greet {
    if lang == "ja" { Ja } else { En }
    // ❌ コンパイルしない — 戻り型は単一の具象型でないと
}

戻り位置の impl Trait は「Trait を実装する 特定の型 を返すが、呼び出し元から隠している」と意味する。静的ディスパッチ — コンパイラが 1 つの型を選び、モノモーフ化、vtable なし。

トレードオフ:

impl Traitdyn Trait
ディスパッチ静的 (コンパイル時)動的 (実行時 vtable)
速度速い (インライン化可能)少し遅い (1 度の間接呼び出し)
異種コレクション❌ 不可✅ 可 (Vec<Box<dyn Trait>>)
オブジェクト安全性関係なし必要 (一部のトレイトはオブジェクト安全でない)

Reth と Revm は 両方 使う。impl がデフォルトで、dyn は異種性が重要なケース (stage のリスト、プラグイン点) のために予約。

オブジェクト安全性 — dyn Trait がコンパイルしない時

dyn Trait として使うにはトレイトが オブジェクト安全 でなければならない。ルールは微妙だが、よく踏むのは:

  • トレイト内の ジェネリックメソッド禁止 (ライフタイム以外で)
  • Self: Sized でないメソッドの Self 戻り型禁止
  • 関連定数禁止

Box<dyn MyTrait> を試してトレイトがオブジェクト安全でないと、「cannot be made into an object」のようなコンパイルエラーが出る。修正は通常、問題のメソッドに where Self: Sized を追加するか、トレイトを分割する。

Reth/Revm コードの 消費者 としてはあまりヒットしないが、なぜソースの一部のトレイトが dyn 化できないかを説明する。

全部まとめる — 実シグネチャを読む

戻ろう:

pub fn add<IT: ITy, H: ?Sized>(context: Ictx<'_, H, IT>) -> Result {

これで単語ずつ読める:

  • pub fn addadd という名前のパブリック関数
  • <IT: ITy, H: ?Sized> — 型パラメータ 2 つ:
    • ITITy トレイトを実装する必要がある (interpreter-types マーカー — 通常 vs トレース vs サンドボックス)
    • H は unsized で良い (なので dyn Host になれる)
  • context: Ictx<'_, H, IT> — 両方でパラメータ化されたコンテキストを取る
  • -> Result — Result を返す

同じ関数、2 つの特殊化パス: 1 つは (具象IT, 具象Host) 用 (完全モノモーフ化、最速)、もう 1 つは (具象IT, dyn Host) 用 (Host 呼び出しは vtable)。?Sized が 2 つ目のパスを可能にする。

これが Revm が「モジュラー」である理由: 同じ opcode 関数が異なる実行モード用に複数バイナリにコンパイルされ、ディスパッチは実行時の分岐ではなく型システムレベルで決まる。

読み物リスト

  1. Rust Book 章 10 (Generics, Traits, Lifetimes)、章 17 (Trait Objects) — 開いて、せめてセクション見出しだけでもスキム。無料リファレンス。
  2. reth/crates で型パラメータが 3 つ以上の関数を見つける。シグネチャを読む。各部品を「この関数はどんな具象形で valid か」に翻訳できるはず。
  3. 思考実験: add シグネチャの H から ?Sized を取り除いたら、&mut dyn Host を渡す呼び出し元にコンパイラはどんな具体的なエラーを出す? (答え: 「the trait Sized is not implemented for dyn Host」)

このレッスンで持ち帰るもの

  • Generics + bounds は Rust 版の「インターフェース」: 「T は何でもいいが、これらの操作をサポートする必要がある」。
  • Sized は暗黙 に各型パラメータに付く; ?Sized が opt out してパラメータをトレイトオブジェクトにできるようにする。
  • dyn Trait は実行時ディスパッチのポインタ + vtable; impl Trait はコンパイル時モノモーフ化。
  • Revm の重い generics 使用は 型レベルでのモジュラリティ — 同じコード、複数特殊化。

中級レッスン 1 でいきなり 1 行目に型パラメータ 3 つと ?Sized をぶつけられた時、それを 5 つの威圧的なトークンではなく、1 つの繋がった文として読めるはず。

まとめ(3行)

  • 4 概念 = ジェネリクス <T>(コンパイル時具象化)+ trait bounds T: Bound(制約)+ ?Sized(trait object 許可)+ dyn / impl Trait(実行時 vs 静的)。
  • Send + Sync + \'static でスレッド間移動 / 共有 / プログラム全体生存、Tokio タスクの土台。
  • 次は Arc / Mutex / RwLock で共有所有権、並行性プリミティブを理解。