6

ネット上には「恐怖のダイヤモンド問題」の解説があふれています。スタックオーバーフローも同様です。私はそれを少し理解していると思いますが、その知識を、似ているが異なるものの理解に変換することに失敗しています.

私の質問は純粋な C++ の質問として始まりますが、答えは MS-COM の仕様に分岐する可能性があります。一般的な問題の質問は次のとおりです。

class Base { /* pure virtual stuff */ };
class Der1 : Base /* Non-virtual! */ { /* pure virtual stuff */ };
class Der2 : Base /* Non-virtual! */ { /* pure virtual stuff */ };
class Join : virtual Der1, virtual Der2 { /* implementation stuff */ };
class Join2 : Join { /* more implementation stuff + overides */ };

これは、従来のダイヤモンド ソリューションではありません。ここで「仮想」は正確に何をしますか?

私の本当の問題は、CodeProject の友人の場所での議論を理解しようとすることです。これには、Flash プレーヤー用の透明なコンテナーを作成するためのカスタム クラスが含まれます。

私はこの場所を楽しみにしようと思いました。次の宣言により、バージョン 10 の Flash Player でアプリがクラッシュすることが判明しました。

class FlashContainerWnd:   virtual public IOleClientSite,
                           virtual public IOleInPlaceSiteWindowless,
                           virtual public IOleInPlaceFrame,
                           virtual public IStorage

デバッグは、関数の実装 (QueryInterface など) に入るときに、さまざまな呼び出し元から、さまざまな呼び出しに対してさまざまな "this" ポインター値を取得することを示しています。しかし、「仮想」を削除するとうまくいきます。クラッシュはなく、同じ「this」ポインター。

何が起こっているのかを正確に理解したいと思います。どうもありがとう。

乾杯アダム

4

5 に答える 5

3

最初の例の仮想継承は何もしません。それらが削除された場合、それらが同じコードにコンパイルされることに賭けます。

Der1仮想的に継承されたクラスは、またはの新しいバージョンをマージする必要があることをコンパイラにフラグするだけDer2です。継承ツリーにはそれぞれ 1 つしか表示されないため、何も行われません。仮想は には影響しませんBase

auto p = new Join2;
static_cast<Base*>(static_cast<Der1*>(p)) !=
      static_cast<Base*>(static_cast<Der2*>(p))

仮想継承は、次に継承されるクラスにのみ影響し、仮想宣言されたインスタンスにのみ影響します。これは予想とは異なりますが、クラスのコンパイル方法の制限です。

class A {};
class B : virtual public A {};
class C : virtual public A {};
class D : public A {};
class E : virtual public A, public B, public C, public D {};
class F : public A, public B, public C, public D {};

F::A != F::B::A or F::C::A or F::D::A
F::B::A == F::C::A
F::D::A != F::B::A or F::C::A or F::A

E::B::A == E::C::A == E::A
E::D::A != E::B::A or E::C::A or E::D::A

E または F ではなく、C および B で A を virtual とマークする必要がある理由の 1 つは、C と B が A のコンストラクターを呼び出さないことを知る必要があるためです。通常、彼らはそれぞれのコピーを初期化します。彼らがダイヤモンドの継承に関与しているとき、彼らはそうしません。ただし、B と C を再コンパイルして A を構築しないようにすることはできません。つまり、C と B は、A のコンストラクターが呼び出されないコンストラクター コードを作成することを事前に知っておく必要があります。

于 2008-11-18T20:09:09.053 に答える
2

COM の例の問題は、virtual キーワードを追加することで、すべてのIOle * インターフェイスが共通の IUnknown 実装を共有していると言っていることだと思います。これを実装するために、コンパイラは複数の v テーブルを作成する必要があるため、派生したクラスに応じて「this」の値が異なります。

COM では、IUnknown のオブジェクトで IQueryInterface を呼び出すときに、オブジェクトによって公開されているすべてのインターフェイスが同じIUnknown を返すことを要求しています ... この実装は明らかに壊れています。

仮想継承がなければ、各 IOle* は名目上、独自の IUnknown 実装を持ちます。ただし、IUnknown は抽象クラスであり、コンパイラにはストレージがなく、すべての IUnknown 実装は FlashContainerWnd から取得されるため、実装は 1 つしかありません。

(OK、最後のビットが弱く聞こえるように...おそらく、言語規則をよりよく理解している人がそれをより明確に説明できるでしょう)

于 2008-11-18T20:05:30.620 に答える
0

あなたの例を試してみようと思いました。私が思いついた:

#include "stdafx.h"
#include <stdio.h>

class Base
{
public:
  virtual void say_hi(const char* s)=0;
};

class Der1 : public Base
{
public:
  virtual void d1()=0;
};

class Der2 : public Base
{
public:
  virtual void d2()=0;
};

class Join : virtual public Der1, virtual public Der2
             // class Join : public Der1, public Der2
{
public:
  virtual void say_hi(const char* s);
  virtual void d1();
  virtual void d2();
};

class Join2 : public Join
{
  virtual void d1();
};

void Join::say_hi(const char* s)
{
  printf("Hi %s (%p)\n", s, this);
}

void Join::d1()
{}

void Join::d2()
{}

void Join2::d1()
{
}

int _tmain(int argc, _TCHAR* argv[])
{
  Join2* j2 = new Join2();
  Join* j = dynamic_cast<Join*>(j2);
  Der1* d1 = dynamic_cast<Der1*>(j2);
  Der2* d2 = dynamic_cast<Der2*>(j2);
  Base* b1 = dynamic_cast<Base*>(d1);
  Base* b2 = dynamic_cast<Base*>(d2);

  printf("j2: %p\n", j2);
  printf("j:  %p\n", j);
  printf("d1: %p\n", d1);
  printf("d2: %p\n", d2);
  printf("b1: %p\n", b1);
  printf("b2: %p\n", b2);

  j2->say_hi("j2");
  j->say_hi(" j");
  d1->say_hi("d1");
  d2->say_hi("d2");
  b1->say_hi("b1");
  b2->say_hi("b2");

  return 0;
}

次の出力が生成されます。

j2: 00376C10
j:  00376C10
d1: 00376C14
d2: 00376C18
b1: 00376C14
b2: 00376C18
Hi j2 (00376C10)
Hi  j (00376C10)
Hi d1 (00376C10)
Hi d2 (00376C10)
Hi b1 (00376C10)
Hi b2 (00376C10)

そのため、Join2 をその基本クラスにキャストすると、異なるポインターが得られる可能性がありますが、say_hi() に渡される this ポインターは常に同じであり、ほとんど予想どおりです。

したがって、基本的に、私はあなたの問題を再現することができず、本当の質問に答えるのが少し難しくなっています.

ワット「仮想」については、ウィキペディアの記事が啓発的であることがわかりましたが、それもダイヤモンドの問題に焦点を当てているようです

于 2009-08-10T10:52:40.787 に答える
0

Caspin が言うように、最初の例は実際には何も役に立ちません。ただし、vpointer を追加して、派生クラスに継承元のクラスを見つける場所を指示します。

これにより、現在作成している可能性のあるダイヤモンド (作成していないもの) は修正されますが、クラス構造が静的でなくなったため、 static_cast を使用できなくなりました。関連する API には詳しくありませんが、Rob Walker が IUnkown について述べていることは、これに関連している可能性があります。

要するに、 「兄弟」クラスと共有すべきではない独自の基本クラスが必要な場合は、通常の継承を使用する必要があります。(悪い例、合成を使用しないのはなぜですか?))

a  a  a
|  |  |
b  c  d <-- b, c and d inherit a normally
 \ | /
   e

仮想継承は、基本クラスをそれらと共有する必要がある場合のためのものです。(a はビークル、b、c、d はビークルのさまざまな専門化、e はこれらの組み合わせ)

   a
 / | \
b  c  d <-- b, c and d inherit a virtually
 \ | /
   d
于 2009-08-16T15:57:46.300 に答える
0

今では少し古くなっていますが、C++ の内部に関連する私が今まで出会った中で最高のリファレンスは、Lippman の Inside The C++ Object Model です。正確な実装の詳細は、コンパイラの出力と一致しない場合がありますが、それを理解することは非常に重要です。

96 ページあたりに仮想継承の説明があり、具体的にはダイヤモンドの問題に対処しています。

詳細はお任せしますが、基本的に仮想継承を使用するには、基本クラスを見つけるために仮想テーブルを検索する必要があります。これは、コンパイル時に基本クラスの場所を計算できる通常の継承には当てはまりません。

(前回、簡単な方法でスタック オーバーフローの質問に答えるための本を勧めたところ、投票数が大幅に増えたので、もう一度同じことが起こるかどうか見てみましょう... :)

于 2008-11-18T22:33:12.040 に答える