6

网上充斥着对“可怕的钻石问题”的解释。StackOverflow 也是如此。我想我理解这一点,但我未能将这些知识转化为理解相似但不同的东西。

我的问题从纯 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 播放器创建透明容器的自定义类。

我想我会试试这个地方的乐趣。事实证明,以下声明使用 Flash 播放器版本 10 使您的应用程序崩溃。

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

调试表明,当输入来自不同调用者的函数实现(QueryInterface 等)时,我得到不同调用的不同“this”指针值。但是删除“虚拟”就可以了!没有崩溃,并且相同的“this”指针。

我想清楚地了解到底发生了什么。非常感谢。

干杯亚当

4

5 回答 5

3

第一个示例中的虚拟继承不做任何事情。我敢打赌,如果它们被删除,它们会编译成相同的代码。

虚拟继承的类只是标记编译器,它应该合并更高版本的Der1or Der2。由于每一个都只出现在继承树中,因此没有做任何事情。虚拟对 没有影响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

A 必须在 C 和 B 而不是 E 或 F 中标记为虚拟的原因之一是 C 和 B 需要知道不要调用 A 的构造函数。通常他们会初始化他们的每个副本。当他们参与钻石继承时,他们不会。但是您不能重新编译 B 和 C 以不构造 A。这意味着 C 和 B 必须提前知道在不调用 A 的构造函数的情况下创建构造函数代码。

于 2008-11-18T20:09:09.053 回答
2

认为您的 COM 示例的问题在于,通过添加 virtual 关键字,您是在说所有 IOle* 接口共享一个通用的 IUnknown 实现。为了实现这一点,编译器必须创建多个 v-table,因此根据派生类的不同,您可以使用不同的“this”值。

COM 要求当您在对象上为 IUnknown 调用 IQueryInterface 时,该对象公开的所有接口都返回相同的IUnknown ...此实现显然会破坏。

如果没有虚拟继承,每个 IOle* 名义上都有自己的 IUnknown 实现。但是,由于 IUnknown 是一个抽象类,并且没有任何存储编译器,并且所有 IUnknown 实现都来自 FlashContainerWnd,因此只有一个实现。

(好吧,所以最后一点听起来很弱......也许对语言规则有更好理解的人可以更清楚地解释它)

于 2008-11-18T20:05:30.620 回答
0

正如 Caspin 所说,您的第一个示例实际上并没有做任何有用的事情。然而,它将做的是添加一个 vpointer 来告诉派生类在哪里可以找到它继承的类。

这修复了您现在可能创建的任何菱形(您没有创建),但由于类结构现在不再是静态的,因此您不能再在其上使用 static_cast。我对所涉及的 API 不熟悉,但 Rob Walker 所说的 IUnkown 可能与此有关。

简而言之,当您需要自己的基类时应该使用正常继承,不应与“兄弟”类共享:(a 是一个容器,b,c,d 是每个都有一个容器的部分,e 组合了这些部分(不好的例子,为什么不使用组合?))

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 回答
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 指针始终是相同的,几乎与预期的一样。

因此,基本上,我无法重现您的问题,因此很难回答您的真实问题。

关于 wat “virtual” 的作用,我发现维基百科上的文章很有启发性,尽管那似乎也关注钻石问题

于 2009-08-10T10:52:40.787 回答