1

これは実際のコードを簡略化したものであり、誰かが既に Foo を実装し、そこから派生していることに気付いていなかったときに犯した本当の間違いです。

#include <iostream>

struct Base {
   virtual ~Base() { }
   virtual void print() = 0;
};

struct OtherBase {
   virtual ~OtherBase() { }
};

struct Foo : public Base { // better to use virtual inheritance?
   virtual void print() { std::cout << "Foo" << std::endl; };
};

struct Bar : public Base { // better to use virtual inheritance?
   virtual void print() { std::cout << "Bar" << std::endl; };
};

// The design is only supposed to implement Base once, but I
// accidentally created a diamond when I inherited from Bar also.
class Derived
   : public OtherBase
   , public Foo
   , public Bar // oops.
{
};

int main() {
   Derived d;
   OtherBase *pO = &d;

   // cross-casting
   if (Base *pBase = dynamic_cast<Base *>(pO))
      pBase->print();
   else
      std::cout << "fail" << std::endl;
}

編集:このコードを実行する必要がないように...

  • そのまま実行すると、「失敗」(望ましくない、デバッグが難しい) が出力されます。
  • 「oops」とマークされた行を削除すると、「Foo」(望ましい動作) が出力されます。
  • 「おっと」を残して 2 つの継承を仮想化すると、コンパイルされません (ただし、少なくとも何を修正すればよいかはわかっています)。
  • 「oops」を削除して仮想化すると、コンパイルされて「Foo」が出力されます (望ましい動作)。

仮想継承を使用すると、結果は良好またはコンパイラ エラーになります。仮想継承がなければ、結果は良好であるか、説明がつかず、デバッグが困難な実行時障害になります。


Foo が既に行っていたことを基本的に複製する Bar を実装すると、動的キャストが失敗し、実際のコードでは問題が発生しました。

最初は、コンパイル エラーがないことに驚きました。次に、仮想継承がないことに気付きました。これにより、GCC で「一意の最終オーバーライド機能がありません」というエラーが発生しました。この設計にはひし形が含まれていないはずなので、意図的に仮想継承を使用しないことにしました。

しかし、Base から派生するときに仮想継承を使用していれば、コードは (おっともなく) 正常に機能し、コンパイル時にダイアモンドについて警告され、実行時にバグを追跡する必要があったはずです。

質問は、仮想継承を使用して将来同様の間違いを犯さないようにすることは許容できると思いますか? ここで仮想継承を使用する正当な技術的理由 (私にはわかります) はありません。設計にひし形があってはならないからです。その設計上の制約を強制するためだけに存在します。

4

3 に答える 3

2

ここにはダイヤモンドはありません!
あなたが作成したのは多重継承でした。各Baseクラスには、独自のBaseのコピーがあります。

pOのタイプはOtherBase*です。
OtherBase*のオブジェクトをタイプBase*に変換する方法はありません。
したがって、動的キャストはNULLポインターを返します。

問題は、実行時の動的キャストにDerivedのオブジェクトへのポインターがあることです。しかし、ここからBaseに到達することはあいまいな操作であるため、NULLで失敗します。dynamic_castはランタイム操作であるため、コンパイラエラーは発生しません。(何かから何かにキャストしようとすると、失敗するとNULLになります(参照を使用している場合はスローします))。

2つのオプション:

  • ただし、参照をキャストすると、dynamic_castで例外をスローすることができます。
  • または、コンパイル時にチェックされるキャストを使用できますstatic_cast <>

これでそれをチェックしてください:

struct Base
{
    Base(int x): val(x) {}
    int val;
...

struct Foo : public Base
{
    Foo(): Base(1)  {}
.... 

struct Bar : public Base
{
    Bar(): Base(2)  {}
....


// In main:
    std::cout << "Foo" << dynamic_cast<Foo&>(d).val << "\n"
              << "Bar" << dynamic_cast<Bar&>(d).val << "\n";


> ./a.exe  
fail
Foo1
Bar2

コンパイル時チェック:

std::cout << static_cast<Base*>(pO) << "\n"; // Should fail to compile.
std::cout << static_cast<Base*>(&d) << "\n"; // Will only fail if ambigious.
                                             // So Fails if Foo and Bar derived from Base
                                             // But works if only one is derived.
于 2009-09-09T22:25:16.950 に答える
2

良い考えではありません。

仮想継承は、事前に計画されている場合にのみ使用されます。今発見したように、多くの場合、すべての子孫クラスがそれを認識している必要があります。基本クラスにデフォルト以外のコンストラクターがある場合、それが常にリーフ クラスによって構築されるという事実について心配する必要があります。

ああ、最後に見たときから状況が変わっていない限り、基本クラスの助けがなければ、仮想基本クラスを派生クラスにダウンキャストすることはできません。

于 2009-09-09T22:12:59.750 に答える
1

最初に考慮すべきことは、継承はコードの再利用ではないということです。そのため、共通の祖先と両側に実装されたメソッドを持つ 2 つのベースから継承する場合は、よく考えてください。

とにかく両方のベースから継承したい場合は、祖先を複製せずに仮想継承を使用する必要があります。これは、例外階層を実装する場合に一般的です。仮想ベースは、最も派生した型のコンストラクターによって直接初期化されることに注意してください。これに注意する必要があります。

于 2009-09-10T20:07:09.403 に答える