C++ 再入門 その25 - 多重継承と仮想継承の関係
前回、ダイアモンド継承という多重継承で起こる問題を説明したのですが、そこでは単に曖昧性が出るのでクラス名を明示すればOKというところだけで済ませてしまいました。
さて、本当の問題はその先にあるのです。ここで継承が通常の継承であるか virtual を使った仮想継承であるかで大きな違いが出るのです。まず、普通に同じ祖先を持つ多重継承を確認してみましょう。
#include <iostream>
using namespace std;
class A {
public:
A() { v = 0; }
void set(int i) { v = i; }
int get(void) { return v; }
private:
int v;
};
class B:public A {};
class C:public A {};
class D:public B, public C {};
int main() {
D d;
d.A::set(1);
cout << "A::v = " << d.A::get() << endl;
d.B::set(1);
cout << "B::v = " << d.B::get() << endl;
d.C::set(1);
cout << "C::v = " << d.C::get() << endl;
return 0;
}
d.set() や d.get() と、もちろん d.D::set() や d.D::get() は、曖昧なのでコンパイルエラーとなります。ここでコードを一捻りしてみます(クラス定義までは同じ)。
int main() {
D d;
d.A::set(1);
d.B::set(2);
d.C::set(3);
cout << "A::v = " << d.A::get() << endl;
cout << "B::v = " << d.B::get() << endl;
cout << "C::v = " << d.C::get() << endl;
return 0;
}
あれぇ何か不思議ですね。クラスAのメンバ変数である v は、B からも C からも名前解決さえすればアクセスできるのですが、B の持つ v と C の持つ v は、別の実体があり、単に A で名前を解決した場合は最初の継承した B と同じ実体を指し C とは別になっているわけです。名前解決は入れ子に出来るので、さらに確認してみましょう。
int main() {
D d;
d.A::set(1);
d.B::set(2);
d.C::set(3);
cout << "A::v = " << d.A::get() << endl;
cout << "B::A::v = " << d.B::A::get() << endl;
cout << "C::A::v = " << d.C::A::get() << endl;
return 0;
}
つまり普通に多重継承した場合、その祖先にあたるクラスは、同じクラスが別のものとして含まれていることになります。祖先となるクラス名のみを使って名前解決した場合、継承時に書かれた順序で辿るのが仕様のようです。いずれにせよ単純な継承の場合は、コンパイル時に実際にアクセスするメンバ変数が決定されるので、メンバ変数の領域がそれだけ必要にはなるものの実行時のコストは変わりません。
ところで仮想継承した場合にはどうなるでしょう。せっかく同じメソッド名を使ってクラスごとに異なる処理を呼び出せるのに名前解決が必要であれば、その多態性を活かすことが出来ないですよね。
#include <iostream>
using namespace std;
class A {
public:
A() { v = 0; }
void set(int i) { v = i; }
int get(void) { return v; }
private:
int v;
};
class BV:public virtual A {};
class CV:public virtual A {};
class D:public BV, public CV {};
int main() {
D d;
d.set(1);
cout << "v = " << d.get() << endl;
return 0;
}
経由するクラス B とクラス C を仮想継承にした BV と CV を使って D を継承すると、何とクラス名を書かずに D のメソッドとして A のメソッドを呼び出すことが出来るようになるのです。もちろん仮想継承しても経由するクラスを明示することは出来るので確認しましょう。
int main() {
D d;
d.A::set(1);
d.BV::set(2);
d.CV::set(3);
d.D::set(4);
cout << "v::A = " << d.A::get() << endl;
cout << "v::BV = " << d.BV::get() << endl;
cout << "v::CV = " << d.CV::get() << endl;
cout << "v::D = " << d.D::get() << endl;
return 0;
}
何と仮想継承することにより、共通のクラス A の実体はひとつにまとめられているようです。
仮想継承
継承 (プログラミング)
これでメソッドの多態性は確保されていることはわかりましたが、継承を仮想にするかどうかで動作が変わることは充分にあるわけです。
組込み現場の「C++」プログラミング 明日から使える徹底入門
今回は中継するクラスをともに仮想で継承しましたが、これが片側だけの場合にどうなるかを考えたくもありません。実際にはもっと複雑な親子関係をもっているクラスもあるので、親クラスの実体が重なっているのか別れているかを把握するのはなかなか難しいことになります。
その8 多重継承と仮想基本クラス
共通する親クラスのメソッドを仮想継承して使う場合、継承の経路毎に仮想関数のアドレスを列挙したテーブルが用意され、経路途中のメソッドを呼び出すのに困らないようになっています。そのため呼び出しの際には仮想関数テーブルを切り替えるコードが追加されることがあり、その場合は気持ちパフォーマンスが悪化することはあります。
仮想継承とメモリ配置
いやあ便利さにはいろいろなコストがかかるものなのですね。多重継承は強力な仕組みではあるのですが、クラス階層の設計には気を使わないと、どこでバグを踏むかわかったものではありません。
というところで継承まわりはいったんココまでとします。
ヘッダ画像は、以下のものを使わせていただきました。https://commons.wikimedia.org/wiki/File:ISO_C%2B%2B_Logo.svg
Jeremy Kratz - https://github.com/isocpp/logos , パブリック・ドメイン,
https://commons.wikimedia.org/w/index.php?curid=62851110による
#プログラミング #プログラミング言語 #プログラミング講座 #CPP #継承 #多重継承 #仮想継承 #多態性 #仮想関数テーブル #ダイヤモンド継承