仮想継承について
仮想継承は
C++プログラミング言語における特別な継承の形式であり、多重継承に伴う問題、特に
菱形継承問題を解決するために使用されます。多重継承では、同じ基底クラスから派生した異なるクラスが存在するため、派生クラスのインスタンスから基底クラスのメンバーを呼び出す場合、どの基底クラスのメンバーを参照するかが不明瞭になることがあります。この曖昧性を解消するのが仮想継承です。
菱形継承とは
菱形継承の状況を具体的に考えてみましょう。例えば、`Bat`(コウモリ)というクラスが、`Mammal`(哺乳類)と`WingedAnimal`(翼のある動物)という基底クラスを持つと仮定します。この時、`bat.eat()`メソッドを呼び出すと、どの`Animal`の`eat()`メソッドなのかが不明になるため、プログラムの動作が不確かになります。`Bat`は`Mammal`でもあり、`WingedAnimal`でもありながら、両方のクラスに共通の基底クラスである`Animal`に対する振る舞いは一つしか存在しないため、混乱が生じます。これが菱形継承の問題です。
継承の構造
継承に関する基本的な構造を理解するために、クラスの表現方法を見てみましょう。
C++では、継承したクラスはメモリ上で親クラスが先に配置され、その後に子クラスが続く形でリスト化されます。この場合、`Bat`のクラス階層は次のようになります:
`(Animal, Mammal, Animal, WingedAnimal, Bat)`です。このリストの中で`Animal`が二度出現しており、このことが曖昧さの原因です。
仮想継承の実装
この曖昧さを解消するために、
C++において仮想継承を利用することができます。具体的には、継承する基底クラスに`virtual`キーワードを付けて宣言します。これにより、`Bat::WingedAnimal`に含まれる`Animal`部分は、`Bat::Mammal`で使用されている`Animal`と同じインスタンスを参照することになります。結果として、`Bat`のインスタンスは常に一つの`Animal`のみを持つこととなり、`bat.eat()`の呼び出しが不明瞭ではなくなります。
この実装は、`Mammal`と`WingedAnimal`それぞれのクラスがvtableポインタを持つことで実現されます。これにより、基底クラス`Animal`のポインタが一つだけになり、メモリ上の管理が容易になります。
メモリオフセット
仮想継承を使用すると、`Mammal`の先頭のメモリオフセットは実行時まで不明のままとなります。そのため、`Bat`の構造は`(vtable, Mammal, vtable, WingedAnimal, Bat, Animal)`といった形が取られます。オブジェクトが持つvtableポインタの数が多いため、オブジェクトのサイズは増加しますが、`Animal`が一つだけになることで、曖昧さが解消されます。さらに、`Mammal`および`WingedAnimal`での各自の`Animal`へのvtableポインタがそれぞれ存在し、正確な動作を確保します。
一方で、他のクラス、例えば`Squirrel`(リス)が`Mammal`を継承する場合、`Squirrel`における`Mammal`オブジェクトのvtableポインタは`Bat`とは異なる値を持つことになります。それでも、両者のクラスが同じサイズであれば、vtableポインタは共通するものになります。
仮想関数テーブルは完全に同一ではありませんが、基本的に距離(オフセット)を格納するという点で類似性があります。
このようにして、仮想継承を用いることで
C++における多重継承の問題を効果的に管理することができます。