28

EDIT3:答える前に、私が何を求めているのかを明確に理解してください(EDIT2と周りにたくさんのコメントがあります)。質問の誤解を明確に示す多くの回答があります(またはありました)(それは私のせいでもあることを知っています、申し訳ありません)

こんにちは、C++ での仮想継承 ( class B: public virtual A {...}) に関する質問を調べましたが、私の質問に対する答えが見つかりませんでした。

仮想継承にはいくつかの問題があることは知っていますが、私が知りたいのは、どのような場合に仮想継承が適切な設計と見なされるかということです。

IUnknownやのようなインターフェースに言及している人々を見かけました。ISerializableまた、そのiostream設計は仮想継承に基づいています。これらは、仮想継承の適切な使用の良い例でしょうか?それは、より良い代替手段がないからですか?それとも、この場合、仮想継承適切な設計であるからですか? ありがとう。

編集:明確にするために、私は実際の例について尋ねています。抽象的なものを与えないでください。私は仮想継承とは何か、どの継承パターンがそれを必要とするかを知っています。私が知りたいのは、複雑な継承の結果だけでなく、それが物事を行うための良い方法である場合です。

EDIT2: つまり、ひし形の階層 (仮想継承の理由)が良い設計である場合を知りたい

4

7 に答える 7

26

インターフェイス階層と対応する実装階層がある場合は、インターフェイス基本クラスを仮想ベースにする必要があります。

例えば

struct IBasicInterface
{
    virtual ~IBasicInterface() {}
    virtual void f() = 0;
};

struct IExtendedInterface : virtual IBasicInterface
{
    virtual ~IExtendedInterface() {}
    virtual void g() = 0;
};

// One possible implementation strategy
struct CBasicImpl : virtual IBasicInterface
{
    virtual ~CBasicImpl() {}
    virtual void f();
};

struct CExtendedImpl : virtual IExtendedInterface, CBasicImpl
{
    virtual ~CExtendedImpl() {}
    virtual void g();
};

通常、これは、基本インターフェースを拡張する多数のインターフェースと、さまざまな状況で必要な複数の実装戦略がある場合にのみ意味があります。このようにして、明確なインターフェース階層があり、実装階層は継承を使用して共通の実装の重複を避けることができます。ただし、Visual Studio を使用している場合は、多くの警告 C4250 が表示されます。

偶発的なスライスを防ぐには、CBasicImplおよびCExtendedImplクラスがインスタンス化可能ではなく、コンストラクターを保存する追加機能を提供しない、より高度なレベルの継承を持つことが通常は最善です。

于 2011-01-05T16:03:32.587 に答える
3

仮想継承は、クラス A が別のクラス B を拡張するが、B にはおそらくデストラクタ以外の仮想メンバー関数がない場合に適した設計上の選択です。B のようなクラスは mixin と考えることができます。この場合、型階層が mixin 型の基本クラスを 1 つだけ必要とする場合、恩恵を受けることができます。

1 つの良い例は、STL の libstdc++ 実装の一部の iostream テンプレートで使用される仮想継承です。たとえば、libstdc++ は次のようにテンプレートを宣言basic_istreamします。

template<typename _CharT, typename _Traits>
class basic_istream : virtual public basic_ios<_CharT, _Traits>

basic_ios<_CharT, _Traits>istream には 1 つの入力 streambuf のみが必要であり、istream の多くの操作は常に同じ機能 (特に、rdbuf唯一の入力 streambuf を取得するメンバー関数) を持つ必要があるため、仮想継承を使用して拡張します。

次に、 type のオブジェクトを読み取るメンバー関数で拡張する class() と、 type のオブジェクトを読み取るメンバー関数でbaz_reader拡張する別の class( ) を作成するとします。と の両方を拡張するクラスを持つことができます。仮想継承が使用されていない場合、およびベースはそれぞれ独自の入力 streambuf を持ちます (おそらくインテントではありません)。おそらく、とbases の両方が同じ streambuf から読み取られるようにする必要があります。extend への仮想継承がなければ、およびのメンバー readbufs を設定することで実現できます。std::istreambazbat_readerstd::istreambatbaz_readerbat_readerbaz_readerbat_readerbaz_readerbat_readerstd::istreamstd::basic_ios<char>baz_readerbat_reader同じ streambuf オブジェクトに基づいていますが、1 つで十分な場合でも、streambuf へのポインターのコピーが 2 つあることになります。

于 2011-01-05T15:51:03.393 に答える
1

Grrr .. 抽象サブタイピングには仮想継承を使用する必要があります。OO の設計原則に従うのであれば、選択の余地はまったくありません。そうしないと、他のプログラマーが他のサブタイプを派生させることができなくなります。

最初に抽象化の例を示します。基本抽象化 A があります。サブタイプ B を作成したいと考えています。サブタイプは必ず別の抽象化を意味することに注意してください。抽象的でない場合は、型ではなく実装です。

ここで、別のプログラマーがやって来て、A のサブタイプ C を作成したいと考えています。クールです。

最後に、さらに別のプログラマーがやってきて、B と C の両方である何かを欲しがっています。もちろん、それも A です。これらのシナリオでは、仮想継承が必須です。

以下は実際の例です: コンパイラからのデータ型のモデリング:

struct function { ..
struct int_to_float_type : virtual function { ..

struct cloneable : virtual function { .. 

struct cloneable_int_to_float_type : 
  virtual function, 
  virtual int_to_float_type 
  virtual cloneable 
{ ..

struct function_f : cloneable_int_to_float_type { 

ここで、functionは関数をint_to_float_type表し、int から float までの関数で構成されるサブタイプを表します。Cloneable関数を複製できる特別なプロパティです。function_f具体的な(非抽象)関数です。

function私が最初に仮想ベースを作成しなかった場合、int_to_float_typeミックスインできなかったことに注意してくださいcloneable(逆もまた同様です)。

一般に、「厳密な」OOP スタイルに従う場合は、常に抽象化のラティスを定義してから、それらの実装を派生させます。抽象化にのみ適用されるサブタイプと実装を厳密に分離します。

Java では、これが強制されます (インターフェイスはクラスではありません)。C++ ではそれは強制されておらず、パターンに従う必要はありませんが、それを認識しておく必要があります。また、作業しているチームまたは作業しているプロジェクトが大きくなるほど、その理由は強くなります。あなたはそれから離れる必要があります。

Mixin 型付けには、C++ で多くのハウスキーピングが必要です。Ocaml では、クラスとクラス型は独立しており、構造 (メソッドを所有するかどうか) によって一致するため、継承は常に便利です。これは、実際には名目上のタイピングよりもはるかに使いやすいです。ミックスインは、名目上の型付けのみを持つ言語で構造型付けをシミュレートする方法を提供します。

于 2011-01-05T16:28:50.843 に答える
0

具体的な例を求めているので、煩わしい参照カウントをお勧めします。この場合、仮想継承が優れた設計であるとは限りませんが、仮想継承は、ジョブを正しく機能させるための適切なツールです。

90年代初頭、私はReferenceCounted他のクラスが派生するクラスを持つクラスライブラリを使用して、参照カウントと参照カウントを管理するためのいくつかのメソッドを提供しました。継承は仮想である必要がありました。そうでない場合、それぞれが非仮想的に派生した複数のベースがある場合ReferenceCounted、複数の参照カウントになってしまいます。仮想継承により、オブジェクトの参照カウントが1つになります。

最近では、他の人との非侵入的な参照カウントshared_ptrがより一般的になっているようですが、クラスがthis他のメソッドに渡される場合でも、侵入的な参照カウントは役立ちます。この場合、外部参照カウントは失われます。また、侵入参照カウントがクラスについて、そのクラスのオブジェクトのライフサイクルがどのように管理されているかを示していることも気に入っています。

[最近はめったに見られないので、侵入的な参照カウントを擁護していると思いますが、気に入っています。]

于 2011-01-06T05:16:14.137 に答える
0

仮想継承は良いことでも悪いことでもありません。他の場合と同じように実装の詳細であり、同じ抽象化が発生するコードを実装するために存在します。これは通常、コードをスーパーランタイムにする必要がある場合に行う正しい方法です。たとえば、一部のCOMオブジェクトをプロセス間で共有する必要があるCOMでは、コンパイラなどはもちろん、通常のC++ライブラリが単純に使用するIUnknownを使用する必要があります。shared_ptr。そのため、私の意見では、通常のC ++コードはテンプレートなどに依存し、仮想継承を必要としないはずですが、特別な場合には完全に必要です。

于 2011-01-05T16:40:32.020 に答える
-3

多重継承を使用する必要がある場合は、仮想継承が必要です。多重継承を避けることによってきれいに/簡単に解決できない問題がいくつかあります。そのような場合 (まれですが)、仮想継承を確認する必要があります。95% の確率で、多重継承を避けることができます (避けるべきです)。

補足として、COM は多重継承の使用を強制しません。線形継承ツリーを持つ IUnknown から (直接的または間接的に) 派生した COM オブジェクトを作成することは可能です (そして非常に一般的です)。

于 2011-01-05T15:25:22.330 に答える
-3

これらの FAQは、仮想継承に関連して考えられるすべての質問に答えています。あなたの質問への回答も (私があなたの質問を正しく認識していれば ;) : FAQ 項目 25.5

于 2011-01-06T06:47:23.480 に答える