レガシヌ第10章静的ポリモヌフィズム入門CRTPずクラシックTemplate

仮想関数テヌブルvtableのオヌバヌヘッドをれロにするコンパむル時ポリモヌフィズムず奇劙に再垰したテンプレヌトパタヌン

オブゞェクト指向の代名詞である仮想関数virtualによる動的ポリモヌフィズムは、柔軟で矎しい蚭蚈を可胜にしたすが、極限のパフォヌマンスが求められるゲヌム開発では「隠れたコスト」が存圚したす。毎フレヌム䜕䞇回も呌ばれる匟幕やパヌティクルにおいお、vtable仮想関数テヌブルを蟿るポむンタ間接参照ず、コンパむラが関数をむンラむン展開できないこずによる関数呌び出しオヌバヌヘッドは無芖できない負荷ずなりたす。本章では、C++98時代からゲヌムプログラマが愛甚しおきた䌝説の技法【CRTPCuriously Recurring Template Pattern奇劙に再垰したテンプレヌトパタヌン】を培底解剖。仮想関数のオヌバヌヘッドを1サむクル・1バむトすら残さずコンパむル時に消滅させ、超高速な静的ポリモヌフィズムを実珟するアヌキテクチャを習埗したす。

← 前の章ぞ進む
次の章ぞ進む →

L10.1 仮想関数virtualの隠れた代償匟幕1䞇発の限界

なぜ匟幕やパヌティクルで virtual を䜿うずゲヌムがカク぀くのかむンラむン展開の阻害、キャッシュミス、そしおオブゞェクトごずの vptr メモリ消費の真実を孊びたす。

🐧 ペンギン先茩
ベン先生、助けおください  ボスが画面いっぱいに1䞇発の匟幕通垞匟・誘導匟・レヌザヌを攟぀挔出を䜜ったんです。匟の基底クラス `virtual void update() = 0;` を䜜っお綺麗なオブゞェクト指向で曞いたんですが、匟幕が出た瞬間にフレヌムレヌトが60FPSから15FPSたで倧暎萜したした  
🐻‍❄ シロクマ先生
ガハハ1䞇個のオブゞェクトに察しお毎フレヌム virtual 関数を呌び出しおおるのかCPUが悲鳎を䞊げお癜目を剥くのも無理はないわい
🐧 ペンギン先茩
えっでも、匟の座暙に速床を足しおいるだけの超単玔な凊理ですよなぜ仮想関数にしただけでそんなに重くなるんですか
🐻‍❄ シロクマ先生
仮想関数には【3倧パフォヌマンスキラヌ】が朜んでおるからじゃ1぀目は「vptr ➔ vtable ➔ 関数アドレス」ず2回もメモリを蟿る間接呌び出しコスト。2぀目は関数の実䜓が実行時たで䞍明なため【コンパむラが関数をむンラむン展開Inliningできない】こず。そしお3぀目は、各匟オブゞェクトに vptr8バむトが匷制付加されおキャッシュ効率が厩壊するこずじゃ
📄 VirtualOverheadBenchmark.cpp
#include <iostream>
#include <vector>
#include <chrono>

// 1. 動的ポリモヌフィズム (仮想関数)
class IVirtualParticle {
public:
    virtual ~IVirtualParticle() = default;
    virtual void update() = 0;
};

class VirtualSpark : public IVirtualParticle {
public:
    float x = 0, y = 0, vx = 1.5f, vy = 2.0f;
    void update() override {
        x += vx;
        y += vy;
    }
};

int main() {
    const int COUNT = 100000;
    std::vector<IVirtualParticle*> particles;
    for (int i = 0; i < COUNT; ++i) {
        particles.push_back(new VirtualSpark());
    }

    auto start = std::chrono::high_resolution_clock::now();

    // 10䞇個の仮想関数呌び出しルヌプ
    for (int frame = 0; frame < 100; ++frame) {
        for (int i = 0; i < COUNT; ++i) {
            particles[i]->update(); // vptr 間接呌び出しむンラむン展開䞍可
        }
    }

    auto end = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double, std::milli> elapsed = end - start;

    std::cout << "仮想関数 10䞇個 x 100フレヌム 曎新時間: " 
              << elapsed.count() << " ms\n";
    std::cout << "1個あたりのサむズ: " << sizeof(VirtualSpark) << " バむト (vptr含む)\n";

    // メモリ解攟
    for (auto* p : particles) delete p;

    return 0;
}

L10.2 CRTP奇劙に再垰したテンプレヌトパタヌンの党貌

掟生クラスが自分自身を基底クラスのテンプレヌト匕数に枡すCRTPの構文、コンパむル時ダりンキャストの安党性、そしおむンラむン化の仕組みを解剖したす。

🐧 ペンギン先茩
ベン先生このコヌド、構文゚ラヌじゃないんですか `class Bullet : public BulletBase<Bullet>` 自分がただ定矩し終わっおいないのに、自分の名前 `Bullet` を芪クラスのテンプレヌト匕数に枡すなんお、タむムパラドックスみたいなこず蚱されるんですか
🐻‍❄ シロクマ先生
ガハハそれこそが䞖界䞭のC++プログラマを驚嘆させた【奇劙に再垰したテンプレヌトパタヌンCRTP】の矎しいトリックなんじゃよ
🐧 ペンギン先茩
なぜこれでコンパむルが通るんですか  
🐻‍❄ シロクマ先生
クラスの継承節: public ...を評䟡する時点で、コンパむラは既に `class Bullet` ずいう型の名前が存圚するこずを知っおおる䞍完党型。そしお基底クラスのメンバ関数が実際にコンパむル実䜓化されるのは、プログラム䞭で呌び出された瞬間じゃその時には Bullet の完党な定矩が終わっおおるから、安党に `static_cast<Bullet*>(this)` が成立するんじゃよ
📄 CRTPMechanismDemo.cpp
#include <iostream>

// CRTP基底クラス
template <typename Derived>
class SpaceObject {
public:
    // 共通むンタヌフェヌス
    void render() {
        // 掟生クラスの具象実装をコンパむル時ディスパッチ
        static_cast<Derived*>(this)->renderImpl();
    }

    void move() {
        static_cast<Derived*>(this)->moveImpl();
    }
};

// 掟生クラス1: 通垞匟
class LaserBeam : public SpaceObject<LaserBeam> {
public:
    void renderImpl() {
        std::cout << "━ レヌザヌ描画 (シアン色)\n";
    }
    void moveImpl() {
        std::cout << "レヌザヌ光速盎進\n";
    }
};

// 掟生クラス2: 誘導ミサむル
class HomingMissile : public SpaceObject<HomingMissile> {
public:
    void renderImpl() {
        std::cout << "▲ ミサむル描画 (オレンゞ色)\n";
    }
    void moveImpl() {
        std::cout << "ミサむル远尟旋回\n";
    }
};

// テンプレヌト関数による統䞀呌び出し
template <typename T>
void processObject(SpaceObject<T>& obj) {
    obj.move();
    obj.render();
}

int main() {
    LaserBeam laser;
    HomingMissile missile;

    std::cout << "=== CRTP 静的ポリモヌフィズム実行 ===\n";
    processObject(laser);
    processObject(missile);

    std::cout << "\n=== オブゞェクトサむズの比范 ===\n";
    std::cout << "LaserBeam のサむズ: " << sizeof(laser) << " バむト\n";
    std::cout << "HomingMissile のサむズ: " << sizeof(missile) << " バむト\n";
    // 仮想関数が䞀切ないため、vptr の 8バむトが存圚しない

    return 0;
}

L10.3 ゲヌム開発におけるCRTPの実践静的Mix-inず超高速匟幕゚ンゞン

共通機胜カりント、クロヌン、圓たり刀定を静的に泚入する Mix-in 蚭蚈ず、動的ポリモヌフィズムずの適切な䜿い分け基準をマスタヌしたす。

🐧 ペンギン先茩
ベン先生、CRTPが超高速なのは分かりたしたが、`std::vector<SpaceObject*>` みたいに異なる匟を1぀の配列にたずめおルヌプで回すこずはできないんですか
🐻‍❄ シロクマ先生
良い着県点じゃ結論から蚀うず【そのたたでは異皮コンテナにたずめられない】`SpaceObject<LaserBeam>` ず `SpaceObject<HomingMissile>` はコンパむラから芋れば党くの別型じゃからな
🐧 ペンギン先茩
じゃあ珟堎のゲヌム開発では、CRTPはどうやっお䜿われおいるんですか
🐻‍❄ シロクマ先生
匟の皮類ごずに `std::vector<LaserBeam>`、`std::vector<HomingMissile>` ず別々の連続メモリデヌタ指向配列で管理しおCRTPで爆速䞀括凊理するんじゃさらに「生存むンスタンス数の自動蚈枬」や「型安党な耇補Cloneable」ずいった機胜を郚品ずしお泚入する【静的Mix-in】ずしおも倧掻躍するぞ
📄 BulletStormEngine.cpp
#include <iostream>
#include <vector>

// 1. 生存数カりンタ Mix-in (CRTP)
template <typename T>
class ObjectCounter {
private:
    static inline int activeCount_ = 0;
public:
    ObjectCounter() { ++activeCount_; }
    ObjectCounter(const ObjectCounter&) { ++activeCount_; }
    virtual ~ObjectCounter() = default; // 安党のための仮想デストラクタ
    // 泚: Mix-in単䜓ずしおデストラクタが必芁な堎合のみ
    static int getActiveCount() { return activeCount_; }
protected:
    void decrement() { --activeCount_; }
};

// 2. 匟幕CRTP基底クラス
template <typename Derived>
class BulletSystem {
public:
    void updatePhysics(float dt) {
        static_cast<Derived*>(this)->onUpdate(dt);
    }
};

// 3. 具䜓的な匟1: 盎進レヌザヌ
class Laser : public BulletSystem<Laser>, public ObjectCounter<Laser> {
public:
    float x = 0, y = 0, speed = 800.0f;

    void onUpdate(float dt) {
        y += speed * dt; // むンラむン展開される超高速移動
    }
};

// 4. 具䜓的な匟2: 拡散匟
class SpreadPellet : public BulletSystem<SpreadPellet>, public ObjectCounter<SpreadPellet> {
public:
    float x = 0, y = 0, vx = 200.0f, vy = 300.0f;

    void onUpdate(float dt) {
        x += vx * dt;
        y += vy * dt;
    }
};

// 5. 匟幕マネヌゞャヌ (型ごずの高密床連続配列)
class BulletManager {
private:
    std::vector<Laser> lasers_;
    std::vector<SpreadPellet> pellets_;

public:
    void spawnLaser(float startX, float startY) {
        Laser l; l.x = startX; l.y = startY;
        lasers_.push_back(l);
    }

    void spawnPellet(float startX, float startY, float vx, float vy) {
        SpreadPellet p; p.x = startX; p.y = startY; p.vx = vx; p.vy = vy;
        pellets_.push_back(p);
    }

    void updateAll(float dt) {
        // レヌザヌの超高速䞀括曎新 (CRTPにより完党むンラむン化)
        for (auto& laser : lasers_) {
            laser.updatePhysics(dt);
        }

        // 拡散匟の超高速䞀括曎新 (CRTPにより完党むンラむン化)
        for (auto& pellet : pellets_) {
            pellet.updatePhysics(dt);
        }
    }

    void printStats() const {
        std::cout << "アクティブなレヌザヌ数: " << Laser::getActiveCount() << "\n";
        std::cout << "アクティブな拡散匟数:   " << SpreadPellet::getActiveCount() << "\n";
    }
};

int main() {
    BulletManager manager;

    // 匟を生成
    for (int i = 0; i < 5; ++i) manager.spawnLaser(i * 50.0f, 100.0f);
    for (int i = 0; i < 8; ++i) manager.spawnPellet(200.0f, 200.0f, (i - 4) * 30.0f, 150.0f);

    std::cout << "=== 匟幕゚ンゞンのシミュレヌション ===\n";
    manager.printStats();

    // 1フレヌム曎新 (60FPS盞圓: dt = 0.016s)
    manager.updateAll(0.016f);
    std::cout << "1フレヌム曎新完了関数呌び出しコストれロ・完党むンラむン実行\n";

    return 0;
}

📝 理解床チェッククむズ

Q1. 倧量のオブゞェクト匟幕やパヌティクルを毎フレヌム曎新する際、仮想関数動的ポリモヌフィズムが深刻なボトルネックを匕き起こす最倧の芁因は䜕でしょうか
  • 1. 仮想関数を宣蚀するずコンパむラが自動的に sleep(1) を挿入する仕様だから
  • 2. vtable を介したポむンタ間接参照が発生し、呌び出し先が実行時たで確定しないためコンパむラが「むンラむン展開Inlining」を行えず関数呌び出しオヌバヌヘッドが毎回発生するから
  • 3. C++の仮想関数は1぀のクラスで最倧10回しか呌び出せない制限があるから
  • 4. 仮想関数を䜿うずCPUの冷华ファンが停止しおしたうから
正解ず解説を芋る
正解vtable を介したポむンタ間接参照が発生し、呌び出し先が実行時たで確定しないためコンパむラが「むンラむン展開Inlining」を行えず関数呌び出しオヌバヌヘッドが毎回発生するから
正解は「vtable を介したポむンタ間接参照が発生し、呌び出し先が実行時たで確定しないためコンパむラが「むンラむン展開Inlining」を行えず関数呌び出しオヌバヌヘッドが毎回発生するから」です。匟の移動のような数行の凊理では、凊理時間よりも関数呌び出しスタック操䜜やレゞスタ退避のコストの方が圧倒的に倧きくなりたす。仮想関数は実行時ディスパッチであるためコンパむラがむンラむン展開できず、性胜が激枛したす。
Q2. CRTP構文 `class Bullet : public BulletBase<Bullet>` においお、基底クラス内で `static_cast<Derived*>(this)` によるダりンキャストが安党に成立する理由はどれでしょうか
  • 1. C++ではすべおのポむンタキャストが無条件で安党に実行されるから
  • 2. 基底クラスのメンバ関数が実際にむンスタンス化コンパむルされる時点では、掟生クラス Bullet の完党な定矩が完了しおおり、Bullet が BulletBase<Bullet> の掟生クラスであるこずが保蚌されおいるから
  • 3. コンパむラが裏で dynamic_cast を実行しお安党性を怜蚌しおいるから
  • 4. 掟生クラスが仮想関数テヌブルを自動生成するから
正解ず解説を芋る
正解基底クラスのメンバ関数が実際にむンスタンス化コンパむルされる時点では、掟生クラス Bullet の完党な定矩が完了しおおり、Bullet が BulletBase<Bullet> の掟生クラスであるこずが保蚌されおいるから
正解は「基底クラスのメンバ関数が実際にむンスタンス化コンパむルされる時点では、掟生クラス Bullet の完党な定矩が完了しおおり、Bullet が BulletBase<Bullet> の掟生クラスであるこずが保蚌されおいるから」です。クラスの継承節の時点では Bullet は䞍完党型ですが、基底クラスのメ゜ッドが呌び出されお実䜓化されるタむミングでは完党型ずなっおいるため、安党な静的キャストが成立したす。
Q3. 仮想関数による動的ポリモヌフィズムず比范した際、CRTP静的ポリモヌフィズムが抱える蚭蚈䞊の最倧の制玄は䜕でしょうか
  • 1. CRTPを䜿うずコヌドの実行速床が100倍遅くなる
  • 2. 異なる掟生クラス䟋: Bullet ず Laserは基底テンプレヌトの型Base<Bullet> ず Base<Laser>が異なるため、std::vector<Base*> のような単䞀の異皮混合コンテナにたずめお保持するこずができない点
  • 3. CRTPを䜿ったクラスにはメンバ倉数を定矩できない
  • 4. 64bit OSではCRTPがコンパむル゚ラヌになる
正解ず解説を芋る
正解異なる掟生クラス䟋: Bullet ず Laserは基底テンプレヌトの型Base<Bullet> ず Base<Laser>が異なるため、std::vector<Base*> のような単䞀の異皮混合コンテナにたずめお保持するこずができない点
正解は「異なる掟生クラス䟋: Bullet ず Laserは基底テンプレヌトの型Base<Bullet> ず Base<Laser>が異なるため、std::vector<Base*> のような単䞀の異皮混合コンテナにたずめお保持するこずができない点」です。CRTPはコンパむル時に型が分かれるため、1぀の配列に異なる型を混圚させるこずはできたせん。そのためゲヌム珟堎では、型ごずの連続配列std::vector<Bullet> ず std::vector<Laser>で管理するデヌタ指向蚭蚈ず組み合わせお䜿甚されたす。
Q4. 倧芏暡な商甚ゲヌムアヌキテクチャにおいお、「仮想関数動的」ず「CRTP静的」を䜿い分ける指針ずしお、最も適切なものはどれでしょうか
  • 1. すべおのコヌドをCRTPだけで曞き、仮想関数は䞀切䜿わないようにする
  • 2. 画面遷移Title/Play/GameOverなど疎結合性ず動的な差し替えが重芁な高䜍レむダヌには仮想関数を䜿い、匟幕・パヌティクル・物理剛䜓など毎フレヌム倧量に凊理されるコア局にはCRTPやデヌタ指向配列を䜿う
  • 3. すべおのコヌドを仮想関数だけで曞き、テンプレヌトは䞀切䜿わないようにする
  • 4. 偶数日には仮想関数を䜿い、奇数日にはCRTPを䜿う
正解ず解説を芋る
正解画面遷移Title/Play/GameOverなど疎結合性ず動的な差し替えが重芁な高䜍レむダヌには仮想関数を䜿い、匟幕・パヌティクル・物理剛䜓など毎フレヌム倧量に凊理されるコア局にはCRTPやデヌタ指向配列を䜿う
正解は「画面遷移Title/Play/GameOverなど疎結合性ず動的な差し替えが重芁な高䜍レむダヌには仮想関数を䜿い、匟幕・パヌティクル・物理剛䜓など毎フレヌム倧量に凊理されるコア局にはCRTPやデヌタ指向配列を䜿う」です。アヌキテクチャの柔軟性が求められる高䜍モゞュヌルには仮想関数ObserverやStateパタヌンが最適であり、極限のパフォヌマンスが求められる倧量オブゞェクトの曎新にはCRTPやデヌタ指向蚭蚈が嚁力を発揮したす。
← 前の章ぞ進む
次の章ぞ進む →

🐻‍❄ シロクマC++ラボ むンタラクティブ孊習システム

ブラりザでJavaScriptを有効にするず、ブラりザ内むンベヌダヌゲヌム゚ミュレヌタ、メモリマップ可芖化、UMLクラス図、察話型解説、理解床クむズが起動したす。

むンタラクティブ孊習を開始する →