C++多重虚继承与COM

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 player 的版本 10 有关。
class FlashContainerWnd:   virtual public IOleClientSite,
                           virtual public IOleInPlaceSiteWindowless,
                           virtual public IOleInPlaceFrame,
                           virtual public IStorage

调试显示,从不同的调用者进入函数实现(例如QueryInterface等),对于不同的调用,我会获得不同的"this"指针值。 但是去除"virtual"就可以解决问题!没有崩溃,并且相同的"this"指针。

我希望能清楚地理解到底发生了什么。非常感谢。

祝好 Adam


我不擅长虚拟继承。但是你的应用程序是否包含从IOle或IStorage转换为FlashContainerWnd的强制转换? - Johannes Schaub - litb
5个回答

3

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

虚继承只是向编译器标记,它应该合并Der1Der2的后续版本。由于继承树中只出现了每个类的一个实例,因此不会执行任何操作。虚拟继承对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的构造函数未被调用的情况下创建构造函数代码。


我不确定你所说的“已声明为虚拟的实例”是什么意思。如果你指的是后续的虚拟继承,那么我的真实世界问题,不幸的是,证明了你的答案是错误的。因为我只有在单一层级上使用虚拟。有虚拟:崩溃。没有虚拟:好的。 - Adam

2
我认为您的COM示例存在问题是因为添加了virtual关键字,这意味着所有IOle*接口共享一个IUnknown实现。为了实现这一点,编译器必须创建多个虚函数表,因此,根据派生类不同,您会得到不同的"this"值。
COM要求在调用对象的IQueryInterface获取IUnknown时,该对象公开的所有接口都返回相同的IUnknown...而此实现显然违反了此规定。
如果没有虚继承,则每个IOle*名义上都有自己的IUnknown实现。但是,由于IUnknown是一个抽象类,并且没有任何存储,编译器和所有IUnknown实现都来自FlashContainerWnd,因此只有一个实现。
(好吧,最后一部分听起来有点含糊不清...也许语言规则掌握得更好的人可以更清楚地解释它)

没错,我认为你是对的。你先获取IUnknown,然后使用QueryInterface获取你想要的接口。你不会像通常的C++方式那样强制转换COM对象。 - gbjbaanb
谢谢,如果我正确理解了你的回答,我应该使用“virtual”,但事实上我必须将其删除以防止崩溃。所以我无法理解这如何回答我的问题。 - Adam

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指针始终相同,这基本上是预期的。

因此,基本上我无法重现您的问题,这使得回答您的真正问题有点困难。

关于"virtual"的作用,我在wikipedia上找到了一篇启发性的文章,尽管它似乎也集中于菱形问题。


0

正如Caspin所说,你的第一个例子实际上并没有做任何有用的事情。但它会添加一个虚指针,告诉派生类从哪里找到继承的类。

这解决了你现在可能创建的任何菱形问题(其实你没有),但由于类结构现在不再是静态的,你不能再对它使用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

0

虽然现在有点过时,但我所遇到的关于C++内部的最佳参考资料是Lippman的《Inside The C++ Object Model》。确切的实现细节可能与您的编译器输出不匹配,但它提供的理解非常有价值。

在第96页左右,有一个关于虚继承的解释,它特别涉及了菱形问题。

我会让你自己阅读详细内容,但基本上使用虚继承需要在虚表中查找以定位基类。这在普通继承中并非如此,其中可以在编译时计算出基类位置。

(上次我采取了简单的方式,只是推荐了一本书来回答堆栈溢出的问题,结果得到了相当多的赞同票,所以让我们看看这次会发生什么... :)


谢谢,但这不是钻石问题,如果您仔细阅读我的问题的话。 - Adam

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接