多重継承の問題点
メモファイルを整理していたら出てきたので載せておく。
2年ぐらい前にデスマってるときに現実逃避にまとめたものなので正しいかどうかはわからない。
重複するメンバ
class Parent1 { public: virtual void message(); }; class Parent2 { public: virtual void message(); }; class Child : public Parent1, public Parent2 { };
このとき、次のようなコードはどうなるのか。
Child* child = new Child(); child->message(); delete child;
child->message()
がParent1::message()
なのかParent2::message()
なのか判別できない(コンパイルエラー)。
次のようにすることでコンパイルは可能。
Child* child = new Child(); child->Parent1::message(); // あるいは // dynamic_cast<CParent1*>(child)->message(); delete child;
しかし、これではParent1::message()
を固定的に指定することになるので、多態性が実現できなくなる。
親が同じクラスの多重継承
class Parent {}; class Child1 : public Parent {}; class Child2 : public Parent {}; class Grandchild : public Child1, public Child2 {};
この場合、Child1の継承するParentとChild2の継承するParentの2つが存在する。
Parent --- Child1 \ Grandchild Parent --- Child2 /
図にするとこんな感じ。
GrandchildからParentのメンバにアクセスしようと思ってParent::message()
としてもどちらのParentか判断できない(コンパイルエラー)。
またChild1がParentのプロパティを変更したとしても、Child2から見たParentは別なので、プロパティは変わってない(ように見える)という問題がある。
ダイヤモンド継承
このような状況でParentを別々のものとして扱いたくない場合は
次のように仮想継承という方法を使う。
class Parent {}; class Child1 : public virtual Parent {}; class Child2 : public virtual Parent {}; class Grandchild : public Child1, public Child2 {};
Parentは1つとなり、Child1とChild2は共に同じParentを継承することになる。いわゆるダイヤモンド継承と言われる形態。
/ Child1 \ Parent Grandchild \ Child2 /
図にするとこんな感じ。
これでGrandchildからParentのメンバにアクセスしても曖昧さはなくなる。
しかし、Child1とChild2がParentのメソッドをオーバライドしているような場合、結局は名前の衝突が起きてしまう。
また、コンストラクタの呼び出し回数の問題から、仮想継承されたクラスのコンストラクタは2つ先のクラスでも呼ぶ必要がある、などの制限が出てくる。
GrandchildのコンストラクタはChild1とChild2のコンストラクタを呼ぶ必要がある。
しかし、Child1とChild2がそれぞれParentのコンストラクタを呼び出すと、Parentのコンストラクタが2回呼ばれてしまうことになるので、このような場合にはParentのコンストラクタは呼ばれないようになっている。
そして次のようにGrandchildから呼ぶことになる。
Grandchild::Grandchild() : Parent(), Child1(), Child2() {}
こうすることで、まずParentのコンストラクタが呼ばれ、次にChild1、Child2のコンストラクタが呼ばれる。