
仮想継承は、基本クラスのメンバー変数のコピーが孫派生クラスに1 つだけ継承されるようにするC++テクニックです。仮想継承がない場合、2 つのクラスとがクラス から継承され、クラス がと の両方から継承されている場合、 にはのメンバー変数のコピーが 2 つ含まれます。1つは 経由、もう 1 つは 経由です。これらは、スコープ解決を使用して個別にアクセスできます。
BCADBCDABC
代わりに、クラスBとがCクラスから仮想的に継承する場合A、クラスのオブジェクトには、Dクラスのメンバー変数のセットが 1 つだけ含まれますA。
この機能は多重継承に最も役立ちます。仮想基底クラスを派生クラスとそこから派生したすべてのクラスの共通サブオブジェクトにするためです。これを使用すると、どの祖先クラスを使用するかという曖昧さを明確にしてダイヤモンド問題をD回避することができます。派生クラス (上記の例)の観点からは、仮想基底クラス ( ) は、基底クラス (または)を介して間接的に派生したクラスではなく、Aの直接の基底クラスであるかのように動作します。[1] [2]DBC
これは、継承が部分の構成ではなくセットの制限を表す場合に使用されます。C++ では、階層全体で共通となることを意図した基本クラスは、virtual キーワードを使用して仮想として示されます。
次のクラス階層を考慮してください。
struct Animal { virtual ~ Animal () = default ; // デフォルトのクラスデストラクタが作成されることを明示的に示します。virtual void Eat () {} };
構造体Mammal : Animal {仮想void Breathe () {} };
構造体WingedAnimal : Animal {仮想void Flap () {} };
// コウモリは翼のある哺乳類です
struct Bat : Mammal , WingedAnimal {};
上で宣言したように、には 2 つの (間接的な) 基本クラスbat.Eatがあるため、 の呼び出しはあいまいであり、どのオブジェクトにも 2 つの異なる基本クラスのサブオブジェクトがあります。したがって、オブジェクトのサブオブジェクトへの参照を直接バインドしようとすると、バインドが本質的にあいまいであるため失敗します。
AnimalBatBatAnimalAnimalBat
Bat bat ; Animal & animal = bat ; // エラー: Bat はどの Animal サブオブジェクトにキャストする必要がありますか? // Mammal::Animal または WingedAnimal::Animal?
曖昧さを解消するには、どちらかの基本クラスのサブオブジェクトに明示的に変換する必要がありますbat。
コウモリ bat ;動物&哺乳類= static_cast < Mammal & > ( bat );動物&翼のある= static_cast < WingedAnimal &> ( bat );
を呼び出すにはEat、同じ曖昧さ回避、つまり明示的な修飾が必要です。static_cast<Mammal&>(bat).Eat()またはstatic_cast<WingedAnimal&>(bat).Eat()あるいは およびbat.Mammal::Eat()。bat.WingedAnimal::Eat()明示的な修飾では、ポインターとオブジェクトの両方に対してより簡単で統一された構文が使用されるだけでなく、静的ディスパッチも可能になるため、おそらくこちらの方が好ましい方法でしょう。
この場合、Animal関係 (BatはAnimal) が 1 回だけ存在することをモデル化したいので、の二重継承はおそらく不要です。Batが でありMammalであるということはWingedAnimal、それが 2 回 であることを意味するわけではありませんAnimal。Animal基本クラスは を実装する契約に対応しBat(上記の「は である」関係は実際には「が の要件を実装する」ことを意味します)、 は契約を 1 回Batだけ実装します。「は1 回だけである」Animalの実際の意味は、 がを実装する方法は 1 つだけであるべきであり、のビューが を食べているのか、のビューが を食べているのかによって 2 つの異なる方法があるべきではないということです。(最初のコード例では、が または のどちらでもオーバーライドされていないため、2 つのサブオブジェクトは実際には同じように動作しますが、これは単なる退化したケースであり、C++ の観点からは違いはありません。)
BatEatMammalBatWingedAnimalBatEatMammalWingedAnimalAnimal
この状況は、継承図がダイヤモンドの形をしているため、 ダイヤモンド継承(ダイヤモンド問題を参照)と呼ばれることがあります。仮想継承は、この問題の解決に役立ちます。
解決策
次のようにクラスを再宣言できます。
構造体Animal {仮想~ Animal () = default ;仮想void Eat () {} };
// Animal を仮想的に継承する 2 つのクラス:
struct Mammal : virtual Animal { virtual void Breathe () {} };
構造体WingedAnimal :仮想Animal {仮想void Flap () {} };
// コウモリは依然として翼のある哺乳類です
struct Bat : Mammal , WingedAnimal {};
Animalの部分は、で使用されるインスタンスと同じBat::WingedAnimalインスタンスになりました。つまり、の表現には共有インスタンスが 1 つだけあるため、 への呼び出しは明確になります。さらに、に変換できる
インスタンスが1 つだけになったため、から への直接キャストも明確になります。 AnimalBat::MammalBatAnimalBat::EatBatAnimalAnimalBat
Animalと の間で親の単一インスタンスを共有する機能はMammal、 またはメンバーと派生クラス内の基底のメンバーとのWingedAnimal間のメモリ オフセットを記録することによって有効になります。ただし、このオフセットは一般に実行時にのみわかるため、 ( 、、、、、 )とする必要があります。を仮想的に継承する継承階層ごとに 1 つずつ、合計2 つのvtableポインターがあります。この例では、 に対して 1 つ、 に対して 1 つです。したがって、オブジェクト サイズは 2 つのポインターだけ増加しましたが、現在は 1 つだけであり、あいまいさはありません。 型 のすべてのオブジェクトは同じ vpointer を使用しますが、各オブジェクトには独自の一意のオブジェクトが含まれます。などの別のクラスが から継承する場合、の部分の vpointer はの部分の vpointer とは通常異なりますが、クラスが と同じサイズであれば偶然同じになることもあります。
MammalWingedAnimalAnimalBatvpointerMammalvpointerWingedAnimalBatAnimalAnimalMammalWingedAnimalAnimalBatBatAnimalMammalSquirrelMammalSquirrelMammalBatSquirrelBat
複数の祖先の追加例
Aこの例では、基本クラスにコンストラクター変数がありmsg、追加の祖先Eが孫クラスから派生する場合を示しますD。
あ / \ 紀元前 \ / だ | え
ここで、は と のA両方で構築される必要があります。さらに、変数 を調べると、クラス が、 と最終派生クラスの間にある中間派生クラスの基底クラスではなく、派生クラスの直接の基底クラスになる様子がわかります。以下のコードは、ここで対話的に調べることができます。DEmsgAA
#include <文字列> #include <iostream>
クラスA { private : std :: string _msg ; public : A ( std :: string x ) : _msg ( x ) {} void test (){ std :: cout << "hello from A: " << _msg << " \n " ; } };
// B、C は A を仮想的に継承します。
class B : virtual public A { public : B () : A ( "b" ){} }; class C : virtual public A { public : C () : A ( "c" ){} };
// B、C は A を仮想的に継承するため、それぞれの子で A を構築する必要があります
// B() および C() コンストラクターは省略できます
class D : public B 、C { public : D () : A ( "d_a" ), B (), C (){} }; // D() コンストラクターは省略class E : public D { public : E () : A ( "e_a" ){} };
// A を構築せずに中断します
// class D: public B,C { public: D():B(),C(){} };
// A を構築せずに中断します
//class E: public D { public: E():D(){} };
int main ( int argc , char ** argv ){ D d ; d . test (); // 出力: "hello from A: d_a"
E e ; e . test (); // 出力: "hello from A: e_a" }
純粋仮想メソッド
純粋仮想メソッドが基本クラスで定義されているとします。派生クラスが基本クラスを仮想的に継承する場合、その派生クラスで純粋仮想メソッドを定義する必要はありません。ただし、派生クラスが基本クラスを仮想的に継承しない場合は、すべての仮想メソッドを定義する必要があります。以下のコードは、ここで対話的に調べることができます。
#include <文字列> #include <iostream>
クラスA { protected : std :: string _msg ; public : A ( std :: string x ) : _msg ( x ) {} void test (){ std :: cout << "hello from A: " << _msg << " \n " ; } virtual void pure_virtual_test () = 0 ; };
// B、C は A を仮想的に継承するため、純粋仮想メソッド pure_virtual_test を定義する必要はありません。
class B : virtual public A { public : B ( std :: string x ) : A ( "b" ){} }; class C : virtual public A { public : C ( std :: string x ) : A ( "c" ){} };
// B、C は A を仮想的に継承するため、それぞれの子で A を構築する必要があります
// ただし、D は B、C を仮想的に継承しないため、A の純粋仮想メソッドを *定義する必要があります*
class D : public B 、C { public : D ( std :: string x ) : A ( "d_a" ), B ( "d_b" ), C ( "d_c" ){} void pure_virtual_test () override { std :: cout << "pure virtual hello from: " << _msg << " \n " ; } };
// 親が純粋仮想メソッドを定義した後は、それを再定義する必要はありません。
class E : public D { public : E ( std :: string x ) : A ( "e_a" ), D ( "e_d" ){} };
int main ( int argc , char ** argv ){ D d ( "d" ); d . test (); // A からの hello: d_a d . pure_virtual_test (); // 純粋仮想 d_a からの hello
E e ( "e" ); e . test (); // A からの hello: e_a e . pure_virtual_test (); // 純粋仮想 hello from: e_a }
参考文献
- ^ Milea, Andrei. 「仮想継承によるダイヤモンド問題の解決」。Cprogramming.com。2010年 3 月 8 日取得。
多重継承によって発生する問題の 1 つに、ダイヤモンド問題があります。この問題の典型的な例は、Bjarne Stroustrup (C++ の考案者) による次の例です。
- ^ McArdell, Ralph (2004-02-14). 「C++/仮想継承とは?」. All Experts . 2010-01-10 にオリジナルからアーカイブ。2010-03-08に取得。
これは、多重継承を使用している場合に必要になることがあります。その場合、クラスが同じ基本クラスを持つ他のクラスから派生する可能性があります。このような場合、仮想継承がないと、オブジェクトには基本クラスが共有する基本型のサブオブジェクトが複数含まれます。これが必要な効果であるかどうかは、状況によって異なります。そうでない場合は、オブジェクト全体がそのような基本クラス サブオブジェクトを 1 つだけ含む基本型に仮想基本クラスを指定して、仮想継承を使用できます。
