仮想継承
CoddyのC++ジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 54/104。
ダイヤモンド問題は、class が共通の base class を共有する2つの class から inherits するときに発生します。特別な処理を行わないと、派生 class は共通の base のコピーを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 クラスには、2つの独立した Animal サブオブジェクトが含まれています。1つは Mammal を通じたもので、もう1つは Bird を通じたものです。これにより、この問題の名前の由来となったダイヤモンド型の inheritance 図が作られます。
仮想 inheritanceは、共通の base のコピーが1つだけ存在するようにすることで、この問題を解決します。共有 base から継承するときは、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 を直接初期化する必要があることに注意してください。仮想継承では、中間クラスに関係なく、最も派生したクラスが仮想基底クラスの構築を担当します。
チャレンジ
簡単仮想継承がダイヤモンド問題をどのように解決するかを示す、worker 管理システムを構築しましょう。TeamLead がDeveloperとManagerの両方から継承し、それらが共通のEmployee base classを共有する hierarchy を作成します。
コードを4つのファイルに分けて整理します。
Employee.h:共通の base classEmployeeを Defineします。- protected な
std::string nameとint id - 両方の値を受け取り、次を出力する constructor:
Employee [<name>] hired with ID <id> - 次を出力する public な
getInfo()method:Employee: <name> (ID: <id>) - 次を出力する virtual destructor:
Employee [<name>] record closed
- protected な
Developer.h:Employeeからvirtual public inheritanceを使用するDeveloperclassを Defineします。- protected な
std::string languagemember - name、id、languageを受け取る constructor:nameとidを
Employeeに渡し、languageを保存して、次を出力します:Developer [<name>] specializes in <language> - 次を出力する public な
code()method:<name> is coding in <language> - 次を出力する destructor:
Developer [<name>] signed off
- protected な
Manager.h:Employeeからvirtual public inheritanceを使用するManagerclassを Defineします。- protected な
int teamSizemember - name、id、team sizeを受け取る constructor:nameとidを
Employeeに渡し、team sizeを保存して、次を出力します:Manager [<name>] leads a team of <teamSize> - 次を出力する public な
manage()method:<name> is managing <teamSize> people - 次を出力する destructor:
Manager [<name>] stepped down
- protected な
main.cpp:4つの inputs(それぞれ別の行)を読み取ります。- Name(string)
- Employee ID(integer)
- Programming language(string)
- Team size(integer)
DeveloperとManagerの両方からpublicに継承するTeamLeadclassを Defineします。- 4つすべてのパラメーターを受け取り、まず
Employee(virtual base)を直接 initializeし、その後DeveloperとManagerを initializeする constructor - constructor は次を出力する必要があります:
TeamLead [<name>] ready to lead and code! getInfo()、code()、manage()をこの順番で callするshowRole()method- 次を出力する destructor:
TeamLead [<name>] promoted out
block scope 内で
TeamLeadobjectをCreateし、showRole()をcallしてから、scope外に出します。blockの後で、次を出力します:Organization restructured!
たとえば、inputsが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のconstructor callとEmployeeのdestructor callがそれぞれ1回だけであることに注目してください。virtual inheritanceにより、共有されるbaseのcopyは1つだけ存在します。virtual inheritanceでは、最も派生したclassがvirtual baseの構築を担当するため、TeamLeadはEmployeeを直接 initializeする必要があります。
自分で試してみよう
#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
// Should print: 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: デストラクタを実装する
// Should print: 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クラス自分で練習してみよう: C++オンラインコンパイラ