仮想継承
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 を直接初期化しなければならないことに注意してください。仮想継承では、中間クラスに関係なく、最も派生したクラスが仮想基底クラスを構築する責任を負います。
チャレンジ
簡単仮想継承がどのようにダイヤモンド問題を解決するかを実証する、従業員管理システムを構築しましょう。TeamLead が Developer と Manager の両方から継承し、その両方が共通の Employee 基底クラスを共有する階層を作成します。
コードは4つのファイルに分けて構成します:
Employee.h: 以下の内容を持つ共通基底クラスEmployeeを定義します:- protected メンバの
std::string nameとint id - 両方の値を受け取り、次のように出力するコンストラクタ:
Employee [<name>] hired with ID <id> - 次のように出力する public メソッド
getInfo():Employee: <name> (ID: <id>) - 次のように出力する仮想デストラクタ:
Employee [<name>] record closed
- protected メンバの
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
- protected メンバの
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
- protected メンバの
main.cpp: 4つの入力(それぞれ別の行)を読み込みます:- 名前 (string)
- 従業員 ID (integer)
- プログラミング言語 (string)
- チームサイズ (integer)
DeveloperとManagerの両方を public 継承するTeamLeadクラスを定義します:- 4つのパラメータすべてを受け取り、
Employee(仮想基底クラス)を直接初期化し、その後にDeveloperとManagerを初期化する必要があるコンストラクタ - コンストラクタは次のように出力する必要があります:
TeamLead [<name>] ready to lead and code! getInfo()、code()、manage()をこの順序で呼び出すshowRole()メソッド- 次のように出力するデストラクタ:
TeamLead [<name>] promoted out
ブロックスコープ内で
TeamLeadオブジェクトを作成し、showRole()を呼び出してから、スコープ外に出します。ブロックの後で、次のように出力します:Organization restructured!
例えば、入力が Alice、101、C++、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つだけ存在することが保証されます。仮想継承を使用する場合、最も派生したクラスが仮想基底クラスの構築に責任を持つため、TeamLead は Employee を直接初期化する必要があります。
チートシート
ダイアモンド継承問題は、あるクラスが共通の基底クラスを共有する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;
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
3コンストラクタとデストラクタ
デフォルトコンストラクタ引数付きコンストラクタコピーコンストラクタムーブコンストラクタコンストラクタ初期化リスト委譲コンストラクタデストラクタ詳解Rule of Three / Five / Zeroまとめ - Stringクラス6演算子オーバーロード
演算子オーバーロード入門算術演算子のオーバーロード比較演算子のオーバーロードストリーム演算子代入演算子のオーバーロード[] と () 演算子のオーバーロード型変換演算子まとめ:Matrix クラス