C++ では、仮想基本クラスとは何ですか?
-
09-06-2019 - |
質問
どういうことなのか知りたいです」仮想基本クラス」とはどういう意味ですか。
例を示します。
class Foo
{
public:
void DoSomething() { /* ... */ }
};
class Bar : public virtual Foo
{
public:
void DoSpecific() { /* ... */ }
};
解決
仮想継承で使用される仮想基本クラスは、多重継承を使用するときに、特定のクラスの複数の「インスタンス」が継承階層に表示されるのを防ぐ方法です。
次のシナリオを考えてみましょう。
class A { public: void Foo() {} };
class B : public A {};
class C : public A {};
class D : public B, public C {};
上記のクラス階層により、次のような「恐ろしいダイヤモンド」が生成されます。
A
/ \
B C
\ /
D
D のインスタンスは、A を含む B と、A も含む C で構成されます。つまり、A の「インスタンス」が 2 つあります (より良い表現が見つかりません)。
このシナリオでは、あいまいさが生じる可能性があります。これを実行するとどうなるか:
D d;
d.Foo(); // is this B's Foo() or C's Foo() ??
仮想継承は、この問題を解決するためにあります。クラスを継承するときに virtual を指定すると、単一のインスタンスのみが必要であることをコンパイラーに伝えることになります。
class A { public: void Foo() {} };
class B : public virtual A {};
class C : public virtual A {};
class D : public B, public C {};
これは、階層に含まれる A の「インスタンス」が 1 つだけであることを意味します。したがって、
D d;
d.Foo(); // no longer ambiguous
ちょっとしたまとめとして参考になれば幸いです。詳細については、以下をお読みください。 これ そして これ. 。良い例も用意されています ここ.
他のヒント
メモリレイアウトについて
余談ですが、Dreaded Diamond の問題は、基本クラスが複数回存在することです。したがって、通常の継承では、次のものがあると考えられます。
A
/ \
B C
\ /
D
ただし、メモリ レイアウトには次のものがあります。
A A
| |
B C
\ /
D
これは、呼び出し時の理由を説明します D::foo()
, 、あいまいさの問題があります。しかし 本物 問題は、メンバー変数を使用したいときに発生します。 A
. 。たとえば、次のものがあるとします。
class A
{
public :
foo() ;
int m_iValue ;
} ;
アクセスしようとするとき m_iValue
から D
, 、階層内に 2 つあるため、コンパイラは抗議します。 m_iValue
, 、 ない1。1 つを変更する場合は、次のようにします。 B::m_iValue
(それは A::m_iValue
の親 B
), C::m_iValue
変更されません (つまり、 A::m_iValue
の親 C
).
ここで仮想継承が役立ちます。仮想継承を使用すると、1 つだけではなく、真のダイヤモンド レイアウトに戻ることができます。 foo()
メソッドだけでなく、唯一無二の m_iValue
.
何が問題になる可能性がありますか?
想像する:
A
にはいくつかの基本的な機能があります。B
それに、ある種のクールなデータ配列を追加します (たとえば)C
これにオブザーバー パターンのような優れた機能を追加します (たとえば、m_iValue
).D
から継承しますB
そしてC
, 、したがって、からA
.
通常の継承では、変更 m_iValue
から D
は曖昧であり、これを解決する必要があります。あったとしても2つある m_iValues
内部 D
, そのことを覚えておいて、2つを同時に更新したほうがよいでしょう。
仮想継承を使用して変更する m_iValue
から D
大丈夫です...しかし...あなたが持っているとしましょう D
. 。それを通して C
インターフェースにオブザーバーを接続しました。そしてそれを通して B
インターフェイスを使用すると、クールな配列を更新します。これには、直接変更されるという副作用があります。 m_iValue
...
の変化としては m_iValue
(仮想アクセサー メソッドを使用せずに) 直接実行され、オブザーバーはそれを「リッスン」します。 C
リスニングを実装するコードが含まれているため、呼び出されません。 C
, 、 そして B
それについては知りません...
結論
あなたの階層にダイヤモンドがある場合、95% の確率でその階層で何か間違ったことをしたことを意味します。
仮想ベースを使用した多重継承を説明するには、C++ オブジェクト モデルの知識が必要です。また、トピックを明確に説明するのは、コメント ボックスではなく記事で行うのが最善です。
このテーマに関する私の疑問をすべて解決してくれた、最も読みやすい説明は次の記事です。 http://www.phpcompiler.org/articles/virtualinheritance.html
これを読んだ後は、このトピックに関する他の内容を読む必要はありません (コンパイラ作成者でない限り)。
仮想ベースクラスは、インスタンス化できないクラスです。直接オブジェクトを作成することはできません。
あなたは 2 つのまったく異なるものを混同していると思います。仮想継承は抽象クラスと同じものではありません。仮想継承は関数呼び出しの動作を変更します。場合によっては、曖昧になる関数呼び出しを解決することもあれば、非仮想継承で期待されるクラス以外のクラスへの関数呼び出しの処理を延期することもあります。
OJ の親切な説明に追加したいと思います。
仮想継承には代償が伴います。あらゆる仮想化と同様に、パフォーマンスが低下します。このパフォーマンスの低下を回避する方法はありますが、おそらくそれほどエレガントではありません。
仮想的に導出してダイヤモンドを破壊する代わりに、ダイヤモンドに別のレイヤーを追加して、次のような結果を得ることができます。
B
/ \
D11 D12
| |
D21 D22
\ /
DD
どのクラスも仮想的に継承せず、すべてパブリックに継承します。クラス D21 と D22 は、おそらく関数をプライベートと宣言することによって、DD にとって曖昧な仮想関数 f() を非表示にします。これらはそれぞれラッパー関数 f1() と f2() を定義し、それぞれがクラスローカル (プライベート) f() を呼び出して競合を解決します。クラス DD は、D11::f() が必要な場合は f1() を呼び出し、D12::f() が必要な場合は f2() を呼び出します。ラッパーをインラインで定義すると、おそらくオーバーヘッドはほぼゼロになります。
もちろん、D11 と D12 を変更できる場合は、これらのクラス内で同じトリックを実行できますが、多くの場合、そうではありません。
多重継承と仮想継承についてすでに述べたことに加えて、Dr Dobb's Journal には非常に興味深い記事があります。 多重継承は有用であると考えられる
少し混乱していますね。いくつかの概念を混同しているかどうかはわかりません。
OP には仮想基本クラスがありません。基本クラスがあるだけです。
仮想継承を行いました。これは通常、複数の派生クラスが基底クラスのメンバーを再生成せずに使用できるように、多重継承で使用されます。
純粋仮想関数を持つ基本クラスはインスタンス化されません。これには、Paul が理解する構文が必要です。これは通常、派生クラスがそれらの関数を定義する必要があるために使用されます。
あなたが何を求めているのか完全に理解できないので、これ以上説明したくありません。
これは、仮想関数への呼び出しが「適切な」クラスに転送されることを意味します。
C++ よくある質問ライト FTW。
つまり、「ダイヤモンド」階層が形成される多重継承シナリオでよく使用されます。仮想継承により、最下位クラスで関数を呼び出し、その関数をその最下位クラスの上のクラス D1 または D2 に解決する必要がある場合に、最下位クラスで作成された曖昧さが解消されます。を参照してください。 FAQ項目 図と詳細については。
などでも使用されています 姉妹代表団, 、強力な機能です(ただし、気の弱い人には向きません)。見る これ よくある質問。
『Effective C++ 第 3 版』の項目 40 (第 2 版では 43) も参照してください。
ダイヤモンド継承ランナブルの使用例
この例は、一般的なシナリオで仮想基本クラスを使用する方法を示しています。ダイヤモンドの相続を解決するために。
#include <cassert>
class A {
public:
A(){}
A(int i) : i(i) {}
int i;
virtual int f() = 0;
virtual int g() = 0;
virtual int h() = 0;
};
class B : public virtual A {
public:
B(int j) : j(j) {}
int j;
virtual int f() { return this->i + this->j; }
};
class C : public virtual A {
public:
C(int k) : k(k) {}
int k;
virtual int g() { return this->i + this->k; }
};
class D : public B, public C {
public:
D(int i, int j, int k) : A(i), B(j), C(k) {}
virtual int h() { return this->i + this->j + this->k; }
};
int main() {
D d = D(1, 2, 4);
assert(d.f() == 3);
assert(d.g() == 5);
assert(d.h() == 7);
}
バーチャルクラスは、 ない 仮想継承と同じです。インスタンス化できない仮想クラス、仮想継承はまったく別のものです。
Wikipedia のほうが私より詳しく説明されています。 http://en.wikipedia.org/wiki/Virtual_inheritance