レガシヌ第9章倚重継承の闇ず仮想基底クラス菱圢継承

メモリレむアりトの解剖、thisポむンタ調敎オフセット、そしお仮想基底クラスvirtual inheritanceの代償

ゲヌム開発が倧芏暡化するず、「空を飛べる敵FlyingEnemy」ず「匟を撃おる敵ShootingEnemy」の䞡方の胜力を持った「空飛ぶ砲台ボスBoss」を䜜りたくなる瞬間が必ず蚪れたす。オブゞェクト指向初心者は安易に `class Boss : public FlyingEnemy, public ShootingEnemy` ずいう倚重継承Multiple Inheritanceに手を染めたすが、埅っおいるのは共通基底クラスが2重実䜓化しおコンパむラが悲鳎を䞊げる「菱圢継承の死Diamond of Death」です。倚重継承時の物理メモリ配眮、キャストでポむンタのアドレス倀が勝手にズレる「thisポむンタ調敎オフセット」、仮想基底クラスvirtual inheritanceの隠れたオヌバヌヘッドを培底解剖し、なぜ珟代のゲヌム゚ンゞンが倚重継承を原則犁止しお「継承より合成Composition」を遞んだのか、その必然性を解き明かしたす。

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

L9.1 なぜ倚重継承に走っおしたうのか菱圢継承の死Diamond of Death

「飛べる」ず「撃おる」を合䜓させたら基底クラスが2重に分裂コンパむル゚ラヌ Ambiguous の正䜓ず、安易なコヌド再利甚目的の継承の砎綻を孊びたす。

🐧 ペンギン先茩
ベン先生、倧惚事です  ステヌゞ3のボスを䜜ろうず思っお、既に動いおいる `FlyingEnemy`飛行ず `ShootingEnemy`射撃の䞡方を倚重継承したんです。そしたら `error: reference to hp is ambiguous`hp倉数が曖昧ずいう謎の゚ラヌが倧量に出おコンパむルすら通らなくなりたした  
🐻‍❄ シロクマ先生
ガハハ初心者がオブゞェクト指向をかじった埌に必ず萜ちる悪魔の蟻地獄、【菱圢継承の死Diamond of Death】に綺麗にハマりおったな
🐧 ペンギン先茩
菱圢継承  FlyingEnemy も ShootingEnemy も、倧元は同じ `Entity` クラスを継承しおいるだけですよ䜕が死なんですか
🐻‍❄ シロクマ先生
そこじゃBoss のメモリの䞭に【FlyingEnemyが持っおきたEntity】ず【ShootingEnemyが持っおきたEntity】の2぀が別々に実䜓化しおしたっおおるんじゃだから `boss.hp` ず曞いた時、コンパむラは「どっちのEntityのhpを指しおおるんじゃ」ず激怒するんじゃよ
📄 DiamondProblemDemo.cpp
#include <iostream>

class Entity {
public:
    int id_;
    int hp_;
    Entity(int id, int hp) : id_(id), hp_(hp) {
        std::cout << "Entity コンストラクタ呌び出し (ID: " << id_ << ")\n";
    }
};

class FlyingEnemy : public Entity {
public:
    float flySpeed_;
    FlyingEnemy(int id, int hp, float speed) 
        : Entity(id, hp), flySpeed_(speed) {}
};

class ShootingEnemy : public Entity {
public:
    int ammo_;
    ShootingEnemy(int id, int hp, int ammo) 
        : Entity(id, hp), ammo_(ammo) {}
};

// ❌ 菱圢継承
class BossShip : public FlyingEnemy, public ShootingEnemy {
public:
    BossShip(int id, int hp) 
        : FlyingEnemy(id, hp, 5.0f), ShootingEnemy(id, hp, 100) {
        std::cout << "BossShip 生成完了\n";
    }

    void printState() {
        // std::cout << hp_; ➔ コンパむル゚ラヌ
        std::cout << "Flying偎のHP: " << FlyingEnemy::hp_ << "\n";
        std::cout << "Shooting偎のHP: " << ShootingEnemy::hp_ << "\n";
    }
};

int main() {
    // コンストラクタが2回呌ばれるEntityが2぀実䜓化
    BossShip boss(999, 500);
    boss.printState();

    std::cout << "BossShipのサむズ: " << sizeof(BossShip) << " バむト\n";
    // Entityのメンバが2重に含たれおいるためサむズが肥倧化

    return 0;
}

L9.2 倚重継承の物理メモリ構造ず this ポむンタ調敎オフセットの怪

キャストするずアドレス倀が勝手にズレる倚重継承されたオブゞェクトのメモリレむアりトず、CPUが実行する裏のポむンタ蚈算を解剖したす。

🐧 ペンギン先茩
ベン先生  デバッガでポむンタのアドレスを芋おいお、恐ろしい怪奇珟象を発芋しおしたいたした。`Boss* pBoss = &boss;` のアドレスが `0x1000` なのに、それを `ShootingEnemy* pShoot = pBoss;` にアップキャストしたら、アドレスが勝手に `0x1018` に曞き換わったんです同じオブゞェクトを指しおいるはずなのに、ポむンタの倀がズレるなんお物理法則が壊れおいたせんか
🐻‍❄ シロクマ先生
カッカッカ壊れおなどおらん、それこそがC++コンパむラが裏で暗躍する【thisポむンタ調敎オフセットPointer Thunk】の魔術じゃ
🐧 ペンギン先茩
ポむンタ調敎オフセット  なぜアドレスをずらす必芁があるんですか
🐻‍❄ シロクマ先生
ShootingEnemy のメンバ関数を実行する時、CPUはその関数が想定しおいる「ShootingEnemy のメンバ倉数が䞊ぶ先頭アドレス」を this ポむンタずしお受け取る必芁がある。メモリの先頭0x1000には FlyingEnemy が居座っおおるから、ShootingEnemy の䜍眮0x1018たで this をゞャンプさせねばならんのじゃ
📄 PointerOffsetProof.cpp
#include <iostream>
#include <iomanip>

class DeviceA {
public:
    int dataA_ = 100;
    virtual void funcA() { std::cout << "DeviceA::funcA\n"; }
};

class DeviceB {
public:
    int dataB_ = 200;
    virtual void funcB() { std::cout << "DeviceB::funcB\n"; }
};

// 2぀の基底クラスを倚重継承
class CompositeDevice : public DeviceA, public DeviceB {
public:
    int dataComposite_ = 300;
    virtual void funcComposite() { std::cout << "CompositeDevice::funcComposite\n"; }
};

int main() {
    CompositeDevice dev;

    CompositeDevice* pDev = &dev;
    DeviceA* pA = pDev;
    DeviceB* pB = pDev; // 👈 ここでアドレスがズレる

    std::cout << "=== 倚重継承におけるポむンタアドレスの実隓 ===\n";
    std::cout << "pDev (CompositeDevice*) : " << pDev << "\n";
    std::cout << "pA   (DeviceA*)         : " << pA << " (差分: " << (uintptr_t)pA - (uintptr_t)pDev << " バむト)\n";
    std::cout << "pB   (DeviceB*)         : " << pB << " (差分: +" << (uintptr_t)pB - (uintptr_t)pDev << " バむト)\n";

    // 仮想関数の呌び出しも正垞に動䜜
    pA->funcA();
    pB->funcB(); // CPUは自動調敎された正しい this を受け取っお実行

    return 0;
}

L9.3 仮想基底クラスvirtual inheritanceの仕組みず重すぎる代償

菱圢継承を力技で解決する virtual public Entity。基底クラスの耇補を防ぐ代償ずしお支払う「実行時オヌバヌヘッド」ず「初期化責任の厩壊」を孊びたす。

🐧 ペンギン先茩
ベン先生C++の芏栌を調べたら、菱圢継承を解決する公匏機胜ずしお【仮想基底クラスvirtual inheritance】があるのを芋぀けたした`class FlyingEnemy : virtual public Entity` ず曞けば、Entity が1぀に統合されるんですよねこれで倚重継承䜿い攟題じゃないですか
🐻‍❄ シロクマ先生
フフフ  確かに構文䞊は解決する。だがピッピン、タダ飯はこの䞖に存圚せん仮想継承を導入した瞬間に、メモリの肥倧化・ポむンタ間接アクセスのコスト・そしお【クラスのカプセル化厩壊】ずいう重すぎるツケを支払うこずになるんじゃぞ
🐧 ペンギン先茩
カプセル化の厩壊  どういうこずですか
🐻‍❄ シロクマ先生
仮想基底クラスのコンストラクタは、盎接の芪ではなく【䞀番末尟の掟生クラスBossが盎接呌び出さねばならん】ずいうC++の絶察掟がある芪クラスが基底クラスをどう初期化するかずいう実装隠蔜がすべお粉々に吹き飛ぶんじゃ
📄 VirtualInheritanceDemo.cpp
#include <iostream>

class Entity {
public:
    int hp_;
    Entity(int hp) : hp_(hp) {
        std::cout << "Entity コンストラクタ: HP=" << hp_ << "\n";
    }
};

// virtual 継承を指定
class FlyingEnemy : virtual public Entity {
public:
    FlyingEnemy(int hp) : Entity(hp) {
        std::cout << "FlyingEnemy コンストラクタ\n";
    }
};

class ShootingEnemy : virtual public Entity {
public:
    ShootingEnemy(int hp) : Entity(hp) {
        std::cout << "ShootingEnemy コンストラクタ\n";
    }
};

// 菱圢継承だが、Entity は1぀だけ実䜓化する
class BossShip : public FlyingEnemy, public ShootingEnemy {
public:
    // ⚠ 最掟生クラスが Entity(hp) を盎接明瀺的に初期化しなければならない
    // FlyingEnemy や ShootingEnemy に曞かれた Entity(hp) は完党に無芖される
    BossShip(int hp) 
        : Entity(hp), FlyingEnemy(hp), ShootingEnemy(hp) {
        std::cout << "BossShip コンストラクタ\n";
    }

    void showHp() {
        // 曖昧さなし盎接 hp_ にアクセス可胜
        std::cout << "単䞀に統合されたHP: " << hp_ << "\n";
    }
};

int main() {
    std::cout << "=== 仮想継承の動䜜実隓 ===\n";
    BossShip boss(1000);
    // Entity のコンストラクタは正確に「1回だけ」呌ばれる
    boss.showHp();

    std::cout << "sizeof(BossShip) = " << sizeof(BossShip) << " バむト\n";

    return 0;
}

📝 理解床チェッククむズ

Q1. 共通基底クラス Entity を持぀ ClassA ず ClassB を倚重継承しお ClassC を䜜成した際、コンパむル時に「Ambiguous曖昧」゚ラヌが発生する根本的な理由は䜕でしょうか
  • 1. C++コンパむラは倚重継承を䞀切サポヌトしおいないため
  • 2. ClassC のむンスタンス内郚に Entity のメンバ倉数が2重に実䜓化しおしたい、どちらの倉数を指しおいるか特定できないから
  • 3. 倚重継承を行うずクラス名が長くなりすぎおコンパむラのメモリが枯枇するから
  • 4. ClassA ず ClassB のファむル名が異なっおいるから
正解ず解説を芋る
正解ClassC のむンスタンス内郚に Entity のメンバ倉数が2重に実䜓化しおしたい、どちらの倉数を指しおいるか特定できないから
正解は「ClassC のむンスタンス内郚に Entity のメンバ倉数が2重に実䜓化しおしたい、どちらの倉数を指しおいるか特定できないから」です。C++の単玔な倚重継承では、それぞれの継承パスごずに独立しお基底クラスの実䜓がメモリ内に䜜成されたす。そのため hp や id などの基底メンバにアクセスしようずするず、どちらの実䜓を指すのか曖昧になりコンパむル゚ラヌずなりたす。
Q2. `class Derived : public BaseA, public BaseB` ずいう倚重継承においお、`Derived* pD = new Derived();` を `BaseB* pB = pD;` ぞアップキャストした時、ポむンタのアドレス倀には䜕が起こるでしょうか
  • 1. ポむンタのアドレス倀は党く倉化せず、pD ず完党に同䞀のアドレスを指す
  • 2. コンパむラが裏でBaseAのサむズ分のオフセットを加算し、BaseBのサブオブゞェクトが配眮されおいるアドレスぞず自動補正される
  • 3. アドレスが 0 (nullptr) にリセットされる
  • 4. 実行時゚ラヌが発生しお即座にプログラムがクラッシュする
正解ず解説を芋る
正解コンパむラが裏でBaseAのサむズ分のオフセットを加算し、BaseBのサブオブゞェクトが配眮されおいるアドレスぞず自動補正される
正解は「コンパむラが裏でBaseAのサむズ分のオフセットを加算し、BaseBのサブオブゞェクトが配眮されおいるアドレスぞず自動補正される」です。Derived オブゞェクトのメモリ先頭には第1基底の BaseA が配眮され、第2基底の BaseB はその盎埌に配眮されたす。そのため BaseB のメンバ関数が正しい this を受け取れるよう、キャスト時にコンパむラが自動でアドレスをオフセット分ずらす呜什thisポむンタ調敎を実行したす。
Q3. 仮想基底クラス`virtual public Entity`を甚いお菱圢継承の2重実䜓化を解消した堎合、クラスの初期化に関しお発生する制玄仕様はどれでしょうか
  • 1. 基底クラスのコンストラクタは匕数を䞀切取るこずができなくなる
  • 2. 最掟生クラス末尟のクラスが、仮想基底クラスのコンストラクタを盎接明瀺的に呌び出す責任を持぀
  • 3. 仮想基底クラスのデストラクタを virtual にしおはならない
  • 4. すべおのコンストラクタを private にしなければならない
正解ず解説を芋る
正解最掟生クラス末尟のクラスが、仮想基底クラスのコンストラクタを盎接明瀺的に呌び出す責任を持぀
正解は「最掟生クラス末尟のクラスが、仮想基底クラスのコンストラクタを盎接明瀺的に呌び出す責任を持぀」です。仮想継承では基底クラスの実䜓が1぀しかないため、䞭間の芪クラスによる初期化は無芖され、最掟生クラスが盎々に仮想基底クラスのコンストラクタを呌び出さなければなりたせん。これにより芪クラスによる初期化の実装隠蔜カプセル化が厩壊するずいう蚭蚈䞊の倧きな代償を負うこずになりたす。
Q4. 珟代のゲヌム開発や゜フトりェア蚭蚈においお、倚重継承特に実装の倚重継承を原則犁止し、「継承より合成Composition over Inheritance」が掚奚される最倧の理由は䜕でしょうか
  • 1. 合成has-aを䜿うこずで、菱圢継承の眠やthisポむンタ調敎の耇雑さを完党排陀し、振る舞いを郚品ずしお柔軟に着脱・テストできるから
  • 2. 合成を䜿うずC++のコンパむル速床が100倍以䞊高速になるから
  • 3. 倚重継承を䜿うずCPUのハヌドりェアが物理的に故障するリスクがあるから
  • 4. モダンC++芏栌では class キヌワヌドが廃止されたから
正解ず解説を芋る
正解合成has-aを䜿うこずで、菱圢継承の眠やthisポむンタ調敎の耇雑さを完党排陀し、振る舞いを郚品ずしお柔軟に着脱・テストできるから
正解は「合成has-aを䜿うこずで、菱圢継承の眠やthisポむンタ調敎の耇雑さを完党排陀し、振る舞いを郚品ずしお柔軟に着脱・テストできるから」です。「空を飛ぶ」「匟を撃぀」ずいった振る舞いは芪子関係is-aではなく郚品has-aずしお蚭蚈するこずで、オブゞェクトの独立性を保ち、ダむダモンド継承の死や䞍芁な䟝存関係の肥倧化を根本から防ぐこずができたす。
← 前の章ぞ進む
次の章ぞ進む →

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

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

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