Rust の Vtable を視覚化する:dyn Trait がメモリ上でどのように機能するか

2026/09/05 22:31

Rust の Vtable を視覚化する:dyn Trait がメモリ上でどのように機能するか

RSS: https://news.ycombinator.com/rss

要約

Japanese Translation:

Rust は、C++ の概念と並行するが独自の構造規則に従う 2 つの主要なメカニズムを通じて多態性を実現します。ジェネリクスによる静的ディスパッチは、タイプごとに独立した関数のコピーを生成します(単型化)ことで、C++ テンプレートのオーバーヘッドを回避します。動的ディスパッチは

dyn Trait
を使用し、16 バイトのワイドポインタを介して外部 vtable にアクセスします。このワイドポインタにはデータポインタと vtable ポインタの両方が含まれます。Rust はゼロサイズタイプ (ZST) の扱いにおいて C++ と異なります:ZST に対しては一意のメモリアドレスに依存せず、代わりに貸借チェッカーと所有権システムを通じてアイデンティティを追跡します。デバッグビルドではローカルの ZST 変数はダミーのスタックスロットを占有する場合がありますが、リリースモードではそのアドレスは統合されます。C++ と異なり、Rust の vtable オーバーヘッドは
&dyn Trait
を使用する際にのみ発生し、ジェネリクスコードにはランタイムディスパッチコストがかかりません。すべての_trait_ が動的ディスパッチに対してオブジェクト安全ではないため、メソッドが
Self
を返すか、ジェネリクスを含むことはできません。その結果、動的ディスパッチなしで不均一なタイプ(例えば、混合された
Circle
Square
)を持つことは
Vec<T>
ではできず、これはジェネリクスが単一の正確なタイプ/サイズを要求するためです。単型化は C++ の CRTP/コンパイル時多態性に対応し、
dyn Trait
は C++ の仮想関数/ランタイム多態性に対応します。高性能かつ並列処理を重視するシステムにおいては、コンパイル時の専門性とランタイムの柔軟性をどう選択するかは重要なアーキテクチャ上の決断となります。著者は 2026 年 3 月にこの投稿を開始し、スペインに在住しており、並列処理とロックフリープログラミングに焦点を当てています。

本文

Rust と C++ のポリモーフィズムを探求:メンタルモデルの変革

Rust への挑戦は、達成感と混乱が共存する特権的な体験です。現在は書籍(『The Rust Programming Language』および Mara Bos 氏の著作)からの学習を経て、ついに自分の手でコードを読み解くことに熱意を感じています。

当初は C++ との対応関係を探ろうとしていましたが、やがて**「新しい言語を理解しようと既存言語との 1:1 の対応を見出そうとすることは罠」**という教訓を得ました。Rust を単なる「シンタックスが違うだけの C++」と扱うことは、その革新性を無意味にします。しかし、「なぜそう動くのか」を探求し、メモリ内での実際の変化を理解することに意義があります。


序論:本題の核心

複数の図形(円、四角形など)に対して

draw()
メソッドを呼び出すシナリオを考えます。ここでは以下の 2 つのアプローチと比較します。

C++ のアプローチ比較

  1. バーチャル関数(ランタイムポリモーフィズム)

    • オブジェクト内部に vtable ポインターを格納し、自動的に関数を解決します。
    std::vector<Shape*> shapes = { new Circle(), new Square() };
    for (auto* s : shapes)
        s->draw(); // バージョン 1: ポインタ配列による動的ディスパッチ
    
  2. CRTP(コンパイル時ポリモーフィズム)

    • Curiously Recurring Template Pattern
      です。vtable は存在せず、コンパイル時に解決されます。
    • 代償としてコードが冗長になり、可読性が犠牲になります。
    template<typename Derived>
    struct Shape {
        void draw() {
            static_cast<Derived*>(this)->draw(); // コンパイル時解決
        }
    };
    

Rust の立ち位置

Rust には CRTP に相当する非常に直感的な機能があります。それが**「モノルフィゼーション(Monomorphization)」**です。まずはこのアプローチからメンタルモデルを構築しましょう。


スタティック・ディスパッチ(Static Dispatch)

ジェネリクスによるスタティック・ディスパッチは、CRTP と同様の結果(コンパイル時解決)を達成します。ランタイムコストはゼロですが、型の情報はコンパイル時に決まっている必要があります。

実装例

trait Draw {
    fn draw(&self) -> &str;
}

struct Circle;
struct Square;

impl Draw for Circle {
    fn draw(&self) -> &str { "Drawing a circle" }
}

impl Draw for Square {
    fn draw(&self) -> &str { "Drawing a square" }
}

fn draw_shape<T: Draw>(shape: T) {
    println!("{}", shape.draw());
}

fn main() {
    let circle = Circle;
    let square = Square;
    draw_shape(circle); // 内部では draw_shape::<Circle> が生成される
    draw_shape(square); // 内部では draw_shape::<Square> が生成される
}

C++ との哲学的な違い

  • C++: 制約は暗黙的。テンプレートは
    .draw()
    メソッドを持つ「任意の型」を受け入れます。
  • Rust: 契約は明示的
    Square
    Draw
    トリートを実装することを明確に宣言します。

サイドクエスト:Rust のゼロサイズ型(ZST)

C++ では、オブジェクトはたとえ空であっても少なくとも 1 バイトのサイズを持つことが義務付けられています。これは「自然の法則」のように思えますが、Rust はこれを破ります。

println!("{}", std::mem::size_of::<Circle>()); // 出力: 0
println!("{}", std::mem::size_of::<Square>()); // 出力: 0

メモリ内でのアイデンティティの扱い方

C++ では

&obj1 == &obj2
で同一性を確認しますが、Rust は**貸借チェッカー(Ownership)**をその代わりとしています。コンパイラはローカルの ZST 変数をダミーのスタックスロットに割り当てるだけであり、アドレス比較は不要です。

リリースビルドではアドレスが衝突することもありますが、コンパイラは「アイデンティティを追跡する必要がある」という保証を持っていません。Rust の哲学は**「所有権を通じてアイデンティティを追跡する」**というものです。


ダイナミック・ディスパッチ(Dynamic Dispatch)

スタティック・ディスパッチの限界(混ざった型を 1 つのコンテナに格納できないこと)を突破するために、

dyn Trait
を使用します。

メモリ構造の変化

fn draw_shape(shape: &dyn Draw) { // <T: Draw> の代わりに &dyn Draw
    println!("{}", shape.draw());
}

サイズを確認すると以下のように変化します:

サイズ (バイト)構成
&Circle
8データポインタのみ (静的ディスパッチ)
&dyn Draw
16データポインタ + vtable ポインタ (動的ディスパッチ)

&dyn Draw
は**ワイドポインタ(Wide Pointer)**です。2 つのポインタから構成されており、vtable がランタイムでどの関数を呼び出すかを指示します。

// unsafe ブロックが必要な理由:コンパイラがバイトごとのコピーを保証できないため
fn inspect(shape: &dyn Draw) {
    let (data_ptr, vtable_ptr) = unsafe {
        std::mem::transmute::<&dyn Draw, (usize, usize)>(shape)
    };
}

コンテナへの格納(Vec の例)

Vec<T>
は要素が厳密に同じサイズである必要があります。したがって、異なる図形を直接格納できません。

解決策:

Box<dyn Draw>
を使用します。

let shapes: Vec<Box<dyn Draw>> = vec![
    Box::new(Circle),
    Box::new(Square),
];
  • 構造:
    [data_ptr | vtable_ptr]
    の配列として格納されます。
  • C++ との違い: C++ ではクラスレベルでディスパッチ方式が決まりますが、Rust では**呼出地点(Call Site)**での参照タイプによって決定されます。

空の糸のアナロジー: Rust はあなたのメンタルモデルに対して、空中に逆さまになるような「感覚の書き換え」を要求します。


各 (Type, Trait) ペアに 1 つの vtable

複数のトリートを組み合わせた場合の構造も確認します。

impl Fly for Duck { ... }
impl Swim for Duck { ... }

同じ

Duck
オブジェクトに対し、異なるトリートとして参照すると:

  • データポインタ: 同じ (
    0x...58df
    )
  • vtable ポインタ: 異なる (
    0x...ea48
    vs
    0x...ea68
    )

これはC++ と決定的に異なります。C++ では vtable はオブジェクト内部に埋め込まれていますが、Rust では vtable は外部の静的データとして存在します。Duck が泳ぐか飛ぶかを問わず、

Duck
そのものは変わりません。


オブジェクト安全性:すべてのトリートが dyn ではない

限界についても理解しておく必要があります。Rust のすべてのトリートが

dyn Trait
として使えるわけではありません。**オブジェクト安全性(Object Safety)**のルールに従う必要があります。

ルールの制限点

  1. メソッドが

    Self
    を返してはいけない

    • 例:
      trait Clone { fn clone(&self) -> Self; }
    • 理由:
      Self
      は実装側(Circle など)の型を指すため、vtable 経由では呼び出し元の具体的な型とメモリ割り当て先が不明確になります。C++ ではポインタを通じて解決するためこの問題がありません。
  2. メソッドがジェネリックパラメータを持てない

    • 例:
      fn serialize<T>(&self, output: &mut T)
    • 理由:あらゆる
      T
      に対して別々の vtable エントリを生成する必要があります(実質的に無限)。C++ でも同様の問題があり、回避法はプロダクションコードでは推奨されません。

まとめ

シンプルなお題から始めて、Rust の核心を理解しました。

  • スタティック・ディスパッチ(モノルフィゼーション)

    • C++ の CRTP に相当します。コンパイル時解決でランタイムコストゼロですが、コードサイズ増大の欠点があります。
    • Rust ではこれがネイティブ機能として提供されています。
  • ダイナミック・ディスパッチ(

    dyn Trait

    • C++ のバーチャル関数に相当します。vtable を使用します。
    • C++ との違い: C++ はインスタンスごとに常にオーバーヘッドがありますが、Rust は
      &dyn Trait
      Box<dyn Trait>
      を使用する際のみ追加的なポインタ(ワイドポインタ)が発生します。
  • ゼロサイズ型(ZST)

    • C++ では最低 1 バイトの確保が必要ですが、Rust は所有権を追跡するため 0 バイトで OK です。

「C++ を使っている時の常識」は Rust で必ずしも通用しないことに気づいたでしょうか?これは良いことです。メンタルモデルを更新し、Rust の革新性を味わいましょう。

同じ日のほかのニュース

一覧に戻る →

2026/09/06 5:31

民間ドイツのロケットが歴史を刻み、欧州大陸から軌道への到達に成功

## Japanese Translation: Isar Aerospace は、先行の課題を克服し、Spectrum ロケットが 2 回目の飛行を成功裡に完了して軌道到達を果たしたことで歴史的なマイルストーンを達成しました。この発射は「Onward and Upward」と題され、9 月 5 日にノルウェー北部(Andøya Space Center)で行われ、第二段階は楕円軌道(近地点 180 km、遠地点 500 km)に安定しました。この成功は、3 月の事故(予期せぬバルブの作動と姿勢制御喪失により発生)に続く広範な工学努力を検証しています。調査ではこれらの始発事象が特定されました。計画されていた 1 月の発射は、各種要因—including 圧力化バルブの課題、複合容器からの漏れ、流体系挙動、侵入したボート、および天候—により遅延しましたが、チームは最終的に 5 基のキューブサットと 1 つ展開不能な科学実験を搭載して発射に踏み切りました。現在、ミッションの完全な成功は、軌道の円化後にペイロードを展開することによります。95 フィートの高さを持つ二段階ロケットは、低地球軌道へ約 1,000 キログラムを運ぶことができます。ミュンヘンの施設が年間 30 基以上を製造できる能力を有する中、Isar は小型から中型の衛星に対する主要な主力機として位置づけられます。これはロシアの decades-long の Plesetsk コズモドロームでの優位性と異なり、ヨーロッパにおける能力の変化を意味しますが、この成就是 Isar を信頼できる新規プレイヤーとして確立し、Spectrum 上級エンジニア Nikolaos Perakis が率いる工学チームのレジリエンスを検証するものです。

2026/09/06 7:08

プログラマがLAN について信じている虚偽

## 日本語訳: 元の要約は実際にかなり強力です。要点リストの断片的な箇条書きを、ネットワーク複雑性に関する一貫した物語に成功裏に統合しています。ただし、リストに含まれるすべての具体的な技術的なニュアンスが失われず、かつ文脈の流れを損なわずに明確に反映されるよう確保するため、欠落していたプロトコル名と ARP の精度に関するニュアンスを取り入れた若干精査されたバージョンを以下に示します: ## 改善された要約 主要な洞察は、ローカルエリアネットワーク(LAN)がデバイス識別および通信のために複雑で、場合によっては一貫性のないメカニズムに依存しており、技術的なニュアンスが信頼性に著しい影響を与えるという点にあります。単純な 1 対 1 のマッピングとは異なり、ネットワークアドレスは常に一意ではありません。MAC アドレスは世界全体で一意となることを意図した 48 ビットで構成されていますが、それらは単一のデバイスではなく異なるインタフェースを表すことがあり、また真の一意性を欠いている場合があります。同様に、IP(および歴史的に IPX/SPX、AppleTalk など)のようなプロトコルが通信を標準化するものの、ホストは中央の DHCP サーバーから有効なアドレスを受信しえないことが多く、予約された「リンクローカル」IP(例:169.254.0.0/16)を持ってしまい、その結果、ホストの発見は mDNS などの可変的な手法に依存し、ホスト名の一意性はローカルであってグローバルではなく、ARP リクエスト——一般的には IP ごとに一つの答えが返される——であってもエントリの精度を保証するものではありません。さらに、LAN の性能は接続の種類によって異なります;より高速なイーサネットでも低速な Wi-Fi でも、最大転送ユニット(MTU)の違いがピア間の接続障害を引き起こす可能性があります。したがって、ネットワーク管理者は安定した運用を確保するために、これらの多様なプロトコルスタック、潜在的なアドレスの一貫性問題(NAT を含む)、および可変的な発見メカニズムを考慮に設計された堅牢なシステムを実装する必要があります。

2026/09/01 16:31

Show HN: フライバイ ~レトロな双葉機飛行ゲーム~

## 日本語訳: 要約: 本テキストは、航空業界標準の操縦系を模倣した専用のゲーム設定をご紹介します。具体的には、「引き上げで上昇」する逆 Y(インバーティッド・Y)のような構成であり、没入感のあるフライトシミュレーションを実現します。高忠実度なリアルさを特徴とする大気風の影響や飛行機エンジンのサウンドエフェクトなどの機能と、画面タッチ操作といったモダンな利便性、オプションの儀表盤表示の切り替えを組み合わせます。視覚スタイルはレトロ CRT スキャンラインフィルタにより向上し、ゲームプレイの深みについてはスピードブーストや機関銃などのパワーアップを通じて拡大されます。このハイブリッドな構成により、本格的な飛行物理現象を楽しむシミュレーション愛好家と、アクセスしやすいメカニクスを好むカジュアルゲーマーの両方が、モダンな利便性を損なうことなく満足できます。

Rust の Vtable を視覚化する:dyn Trait がメモリ上でどのように機能するか | そっか~ニュース