Menu
Coddy logo textTech

仮想継承

CoddyのC++ジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 54/104。

ダイヤモンド問題は、あるクラスが共通の基底クラスを共有する2つのクラスから継承する場合に発生します。特別な処理を行わない場合、派生クラスは共通の基底クラスのコピーを2つ持つことになり、曖昧さやメモリの浪費の原因となります。

class Animal {
public:
    int age;
};

class Mammal : public Animal {};
class Bird : public Animal {};

class Bat : public Mammal, public Bird {};

Bat b;
b.age = 5;  // エラー: 曖昧です - どの 'age' ですか?

Batクラスは、Mammalを経由するものとBirdを経由するものの、2つの別々のAnimalサブオブジェクトを含んでいます。これにより、この問題の名前の由来となったダイヤモンド型の継承図が作成されます。

仮想継承は、共通の基底クラスのコピーが1つだけ存在することを保証することで、この問題を解決します。共有された基底クラスから継承する際に、virtualキーワードを追加します:

class Animal {
public:
    int age;
    Animal(int a = 0) : age(a) {}
};

class Mammal : virtual public Animal {
public:
    Mammal(int a = 0) : Animal(a) {}
};

class Bird : virtual public Animal {
public:
    Bird(int a = 0) : Animal(a) {}
};

class Bat : public Mammal, public Bird {
public:
    Bat(int a) : Animal(a), Mammal(a), Bird(a) {}
};

Bat b(5);
b.age = 10;  // 動作します! 'age' は1つだけ存在します

Bat はそのコンストラクタ内で Animal を直接初期化しなければならないことに注意してください。仮想継承では、中間クラスに関係なく、最も派生したクラスが仮想基底クラスを構築する責任を負います。

challenge icon

チャレンジ

簡単

仮想継承がどのようにダイヤモンド問題を解決するかを実証する、従業員管理システムを構築しましょう。TeamLeadDeveloperManager の両方から継承し、その両方が共通の Employee 基底クラスを共有する階層を作成します。

コードは4つのファイルに分けて構成します:

  • Employee.h: 以下の内容を持つ共通基底クラス Employee を定義します:
    • protected メンバの std::string nameint id
    • 両方の値を受け取り、次のように出力するコンストラクタ:Employee [<name>] hired with ID <id>
    • 次のように出力する public メソッド getInfo()Employee: <name> (ID: <id>)
    • 次のように出力する仮想デストラクタ:Employee [<name>] record closed
  • Developer.h: Employee から virtual public 継承する Developer クラスを定義します:
    • protected メンバの std::string language
    • name、id、language を受け取り、name と id を Employee に渡し、language を保存して次のように出力するコンストラクタ:Developer [<name>] specializes in <language>
    • 次のように出力する public メソッド code()<name> is coding in <language>
    • 次のように出力するデストラクタ:Developer [<name>] signed off
  • Manager.h: Employee から virtual public 継承する Manager クラスを定義します:
    • protected メンバの int teamSize
    • name、id、team size を受け取り、name と id を Employee に渡し、team size を保存して次のように出力するコンストラクタ:Manager [<name>] leads a team of <teamSize>
    • 次のように出力する public メソッド manage()<name> is managing <teamSize> people
    • 次のように出力するデストラクタ:Manager [<name>] stepped down
  • main.cpp: 4つの入力(それぞれ別の行)を読み込みます:
    1. 名前 (string)
    2. 従業員 ID (integer)
    3. プログラミング言語 (string)
    4. チームサイズ (integer)

    DeveloperManager の両方を public 継承する TeamLead クラスを定義します:

    • 4つのパラメータすべてを受け取り、Employee(仮想基底クラス)を直接初期化し、その後に DeveloperManager を初期化する必要があるコンストラクタ
    • コンストラクタは次のように出力する必要があります:TeamLead [<name>] ready to lead and code!
    • getInfo()code()manage() をこの順序で呼び出す showRole() メソッド
    • 次のように出力するデストラクタ:TeamLead [<name>] promoted out

    ブロックスコープ内で TeamLead オブジェクトを作成し、showRole() を呼び出してから、スコープ外に出します。ブロックの後で、次のように出力します:Organization restructured!

例えば、入力が Alice101C++5 の場合:

Employee [Alice] hired with ID 101
Developer [Alice] specializes in C++
Manager [Alice] leads a team of 5
TeamLead [Alice] ready to lead and code!
Employee: Alice (ID: 101)
Alice is coding in C++
Alice is managing 5 people
TeamLead [Alice] promoted out
Manager [Alice] stepped down
Developer [Alice] signed off
Employee [Alice] record closed
Organization restructured!

Employee のコンストラクタ呼び出しと Employee のデストラクタ呼び出しがそれぞれ1回ずつしかないことに注目してください。仮想継承により、共有基底クラスのコピーが1つだけ存在することが保証されます。仮想継承を使用する場合、最も派生したクラスが仮想基底クラスの構築に責任を持つため、TeamLeadEmployee を直接初期化する必要があります。

チートシート

ダイアモンド継承問題は、あるクラスが共通の基底クラスを共有する2つのクラスから継承したときに発生し、その結果、基底クラスのコピーが2つ作成され、曖昧さが生じます。

class Animal {
public:
    int age;
};

class Mammal : public Animal {};
class Bird : public Animal {};

class Bat : public Mammal, public Bird {};

Bat b;
b.age = 5;  // Error: ambiguous - which 'age'?

仮想継承は、共通の基底クラスのコピーが1つだけ存在することを保証することで、ダイアモンド継承問題を解決します。継承時に virtual キーワードを使用します:

class Animal {
public:
    int age;
    Animal(int a = 0) : age(a) {}
};

class Mammal : virtual public Animal {
public:
    Mammal(int a = 0) : Animal(a) {}
};

class Bird : virtual public Animal {
public:
    Bird(int a = 0) : Animal(a) {}
};

class Bat : public Mammal, public Bird {
public:
    Bat(int a) : Animal(a), Mammal(a), Bird(a) {}
};

Bat b(5);
b.age = 10;  // Works! Only one 'age' exists

仮想継承では、中間のクラスに関係なく、最も派生したクラスがコンストラクタで仮想基底クラスを直接初期化する必要があります。

自分で試してみよう

#include <iostream>
#include <string>
#include "Developer.h"
#include "Manager.h"

using namespace std;

// TODO: Developer と Manager の両方を公開継承する TeamLead クラスを定義します
// 注意: 仮想継承を使用する場合、TeamLead は Employee (仮想基底クラス) を直接初期化する必要があります
class TeamLead : public Developer, public Manager {
public:
    // TODO: name, id, language, teamSize を受け取るコンストラクタを実装します
    // 初期化順序: 最初に Employee (仮想基底クラス)、次に Developer、その次に Manager
    // 出力内容: TeamLead [<name>] ready to lead and code!
    TeamLead(const std::string& name, int id, const std::string& language, int teamSize)
        : Employee(name, id),
          Developer(name, id, language),
          Manager(name, id, teamSize) {
        // TODO: コンストラクタのメッセージを出力します
    }

    // TODO: showRole() メソッドを実装します
    // getInfo(), code(), manage() をこの順序で呼び出す必要があります
    void showRole() {
        // TODO: 3つのメソッドを呼び出します
    }

    // TODO: デストラクタを実装します
    // 出力内容: TeamLead [<name>] promoted out
    ~TeamLead() {
        // TODO: デストラクタのメッセージを出力します
    }
};

int main() {
    // 入力を読み取ります
    string name;
    int id;
    string language;
    int teamSize;

    getline(cin, name);
    cin >> id;
    cin.ignore();
    getline(cin, language);
    cin >> teamSize;

    // TODO: ブロックスコープ内で TeamLead オブジェクトを作成します
    // showRole() を呼び出し、その後スコープから外れるようにします
    {
        // TODO: TeamLead を作成し、showRole() を呼び出します
    }

    // ブロックの後に最終メッセージを出力します
    cout << "Organization restructured!" << endl;

    return 0;
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン