447

「仮想基底クラス」とは何か、それが何を意味するのか知りたいです。

例を示しましょう:

class Foo
{
public:
    void DoSomething() { /* ... */ }
};

class Bar : public virtual Foo
{
public:
    void DoSpecific() { /* ... */ }
};
4

11 に答える 11

571

仮想継承で使用される仮想基本クラスは、多重継承を使用するときに、特定のクラスの複数の「インスタンス」が継承階層に表示されるのを防ぐ方法です。

次のシナリオを検討してください。

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

これはミニサマリーです。詳細については、thisthisをお読みください。良い例もここにあります。

于 2008-08-22T01:45:02.770 に答える
281

メモリ配置について

補足として、Dreaded Diamond の問題は、基本クラスが複数回存在することです。したがって、通常の継承では、次のものがあると考えられます。

  A
 / \
B   C
 \ /
  D

しかし、メモリ レイアウトには、次のものがあります。

A   A
|   |
B   C
 \ /
  D

D::foo()これは、 を呼び出すと、あいまいな問題が発生する理由を説明しています。しかし、本当の問題は、のメンバー変数を使用する場合に発生しますA。たとえば、次のようにします。

class A
{
    public :
       foo() ;
       int m_iValue ;
} ;

m_iValueからアクセスしようとするDと、コンパイラは抗議します。これは、階層内にm_iValue1 つではなく 2 つが表示されるためです。たとえば、1 つを変更してもB::m_iValue(それは のA::m_iValue親ですB)、C::m_iValue変更されません (それは のA::m_iValue親ですC)。

これは、仮想継承が便利な場所です。これを使用すると、1 つのfoo()メソッドだけでなく、1 つだけのm_iValue.

何が問題になる可能性がありますか?

想像:

  • Aいくつかの基本的な機能があります。
  • Bそれにある種のクールなデータ配列を追加します(たとえば)
  • Cオブザーバー パターンのようなクールな機能を追加します (たとえば、 on m_iValue)。
  • DBおよびから継承しC、したがって から継承しAます。

通常の継承では、 m_iValuefromの変更Dがあいまいであり、これを解決する必要があります。だとしても のm_iValuesDに 2 つあるので、それを覚えておいて 2 つ同時に更新した方がいいです。

仮想継承では、m_iValueからの変更Dは問題ありません...しかし...あなたが持っているとしましょうD。そのCインターフェースを介して、オブザーバーを接続しました。そして、そのBインターフェースを介して、クールな配列を更新します。これには、直接変更するという副作用がありm_iValueます...

の変更は(仮想アクセサー メソッドを使用せずに) 直接行われるため、リッスンを実装するコードが にあり、それを認識していないためm_iValue、オブザーバーの「リッスン」は呼び出されません...CCB

結論

ヒエラルキーにひし形がある場合、95% の確率でそのヒエラルキーに何か問題があることを意味します。

于 2008-09-21T23:06:14.070 に答える
10

仮想基本クラスは、インスタンス化できないクラスです。仮想基本クラスから直接オブジェクトを作成することはできません。

あなたは2つの非常に異なることを混同していると思います。仮想継承は抽象クラスと同じものではありません。仮想継承は、関数呼び出しの動作を変更します。あいまいな関数呼び出しを解決する場合もあれば、非仮想継承で期待されるクラス以外のクラスへの関数呼び出し処理を延期する場合もあります。

于 2008-08-22T01:47:36.913 に答える
7

OJの親切な説明に付け加えたいと思います。

仮想継承には代償が伴います。仮想のすべてのものと同様に、パフォーマンスが低下します。このパフォーマンスヒットを回避する方法がありますが、おそらくエレガントではありません。

仮想的に導出してダイアモンドを壊す代わりに、ダイアモンドに別のレイヤーを追加して、次のようなものを取得できます。

   B
  / \
D11 D12
 |   |
D21 D22
 \   /
  DD

どのクラスも事実上継承せず、すべてが公的に継承します。次に、クラスD21およびD22は、おそらく関数privateを宣言することにより、DDにとってあいまいな仮想関数f()を非表示にします。それぞれがラッパー関数f1()とf2()をそれぞれ定義し、それぞれがクラスローカル(プライベート)f()を呼び出して、競合を解決します。クラスDDは、D11 :: f()が必要な場合はf1()を呼び出し、D12 :: f()が必要な場合はf2()を呼び出します。ラッパーをインラインで定義すると、オーバーヘッドはほぼゼロになります。

もちろん、D11とD12を変更できる場合は、これらのクラス内で同じトリックを実行できますが、多くの場合、そうではありません。

于 2008-08-22T02:03:04.067 に答える
6

多重継承と仮想継承についてすでに述べたことに加えて、ドブ博士のジャーナルに非常に興味深い記事があります:多重継承は有用であると考えられています

于 2008-09-22T00:58:41.037 に答える
3

ダイヤモンド継承ランナブル使用例

この例は、一般的なシナリオで仮想基本クラスを使用する方法を示しています: ダイヤモンドの継承の問題を解決します。

次の実際の例を検討してください。

main.cpp

#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);
}

コンパイルして実行します。

g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out

を削除するvirtualと:

class B : public virtual A

A を介して 2 回継承された D メンバーとメソッドを解決できないという GCC に関するエラーの壁が表示されます。

main.cpp:27:7: warning: virtual base ‘A’ inaccessible in ‘D’ due to ambiguity [-Wextra]
   27 | class D : public B, public C {
      |       ^
main.cpp: In member function ‘virtual int D::h()’:
main.cpp:30:40: error: request for member ‘i’ is ambiguous
   30 |         virtual int h() { return this->i + this->j + this->k; }
      |                                        ^
main.cpp:7:13: note: candidates are: ‘int A::i’
    7 |         int i;
      |             ^
main.cpp:7:13: note:                 ‘int A::i’
main.cpp: In function ‘int main()’:
main.cpp:34:20: error: invalid cast to abstract class type ‘D’
   34 |     D d = D(1, 2, 4);
      |                    ^
main.cpp:27:7: note:   because the following virtual functions are pure within ‘D’:
   27 | class D : public B, public C {
      |       ^
main.cpp:8:21: note:    ‘virtual int A::f()’
    8 |         virtual int f() = 0;
      |                     ^
main.cpp:9:21: note:    ‘virtual int A::g()’
    9 |         virtual int g() = 0;
      |                     ^
main.cpp:34:7: error: cannot declare variable ‘d’ to be of abstract type ‘D’
   34 |     D d = D(1, 2, 4);
      |       ^
In file included from /usr/include/c++/9/cassert:44,
                 from main.cpp:1:
main.cpp:35:14: error: request for member ‘f’ is ambiguous
   35 |     assert(d.f() == 3);
      |              ^
main.cpp:8:21: note: candidates are: ‘virtual int A::f()’
    8 |         virtual int f() = 0;
      |                     ^
main.cpp:17:21: note:                 ‘virtual int B::f()’
   17 |         virtual int f() { return this->i + this->j; }
      |                     ^
In file included from /usr/include/c++/9/cassert:44,
                 from main.cpp:1:
main.cpp:36:14: error: request for member ‘g’ is ambiguous
   36 |     assert(d.g() == 5);
      |              ^
main.cpp:9:21: note: candidates are: ‘virtual int A::g()’
    9 |         virtual int g() = 0;
      |                     ^
main.cpp:24:21: note:                 ‘virtual int C::g()’
   24 |         virtual int g() { return this->i + this->k; }
      |                     ^
main.cpp:9:21: note:                 ‘virtual int A::g()’
    9 |         virtual int g() = 0;
      |                     ^
./main.out

GCC 9.3.0、Ubuntu 20.04 でテスト済み。

于 2016-12-07T19:52:13.310 に答える
1

これは、仮想関数の呼び出しが「正しい」クラスに転送されることを意味します。

C ++ FAQLiteFTW

つまり、「ひし形」階層が形成される多重継承シナリオでよく使用されます。そのクラスで関数を呼び出し、関数をその最下位クラスの上のクラスD1またはD2に解決する必要がある場合、仮想継承は最下位クラスで作成されたあいまいさを解消します。図と詳細については、FAQ項目を参照してください。

また、強力な機能である姉妹代表団でも使用されます(ただし、心の弱い人向けではありません)。このFAQを参照してください。

効果的なC++第3版の項目40(第2版の43)も参照してください。

于 2008-08-22T01:42:14.990 に答える
1

あなたは少し混乱しています。あなたがいくつかの概念を混同しているかどうかはわかりません。

OPに仮想基本クラスがありません。基本クラスがあります。

あなたは仮想継承をしました。これは通常、多重継承で使用されるため、複数の派生クラスは基本クラスのメンバーを再現せずに使用します。

純粋仮想関数を持つ基本クラスはインスタンス化されません。これには、Paulが取得する構文が必要です。これは通常、派生クラスがこれらの関数を定義する必要があるために使用されます。

私はあなたが求めているものを完全に理解していないので、これについてこれ以上説明したくありません。

于 2008-08-22T01:48:04.977 に答える
0

仮想クラスは仮想継承と同じではありません。インスタンス化できない仮想クラス。仮想継承はまったく別のものです。

ウィキペディアは私ができるよりもそれをよく説明しています。http://en.wikipedia.org/wiki/Virtual_inheritance

于 2008-08-22T01:52:04.920 に答える