网上满是"dreaded diamond problem"的解释。 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 */ };
这是不是经典钻石解决方案。究竟“虚拟”在这里做什么?
我真正的问题是尝试理解discussion over at our friends' place at CodeProject.它涉及一个自定义类,用于为Flash播放器创建透明容器。
我以为我会尝试这个地方玩得开心。事实证明,以下声明会使您的应用程序崩溃,其中包含Flash播放器的第10版。
class FlashContainerWnd: virtual public IOleClientSite,
virtual public IOleInPlaceSiteWindowless,
virtual public IOleInPlaceFrame,
virtual public IStorage
调试显示,当从不同的调用者进入函数实现(QueryInterface等)时,我得到不同调用的不同“this” - 指针值。 但删除“虚拟”可以解决问题!没有崩溃,同样是“这个” - 指针。
我想清楚地了解到底发生了什么。非常感谢。
干杯 亚当
答案 0 :(得分:3)
第一个示例中的虚拟继承不执行任何操作。如果删除它们,我会打算编译成相同的代码。
虚拟继承的类只标记编译器应该合并更高版本的Der1
或Der2
。由于每个中只有一个出现在继承树中,所以没有做任何事情。虚拟对Base
没有影响。
auto p = new Join2;
static_cast<Base*>(static_cast<Der1*>(p)) !=
static_cast<Base*>(static_cast<Der2*>(p))
虚拟继承仅影响下一个继承的类,并且仅适用于已被delcared虚拟的实例。这与你期望的相反,但它是对编译类的方式的限制。
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的构造函数。
答案 1 :(得分:2)
我认为您的COM示例的问题是,通过添加virtual关键字,您说所有IOle *接口共享一个共同的IUnknown实现。为了实现这一点,编译器必须创建多个v表,因此根据它所导出的派生类,您可以使用不同的“this”值。
COM要求当你在IUnknown对象上调用IQueryInterface时,对象公开的 ALL 接口会返回相同的 IUnknown ...这个实现明显地破坏了。< / p>
如果没有虚拟继承,每个IOle *名义上都有自己的IUnknown实现。但是,由于IUnknown是一个抽象类,并且编译器没有任何存储空间,并且所有IUnknown实现都来自FlashContainerWnd,因此只有一个实现。
(好吧,所以最后一点听起来很弱......也许有更好地掌握语言规则的人可以更清楚地解释它)
答案 2 :(得分:0)
现在有点过时了,但我遇到过的关于C ++内部的最佳参考是Lippman的Inside the C ++ Object Model。确切的实现细节可能与编译器的输出不匹配,但它提供的理解非常有价值。
第96页左右有虚拟继承的解释,它专门解决钻石问题。
我将让您阅读详细信息,但基本上使用虚拟继承需要在虚拟表中进行查找才能找到基类。在普通继承中不是这种情况,其中基类位置可以在编译时计算。
(上一次我采取了简单的方法,只是推荐了一本书来回答堆栈溢出问题,我得到了相当多的投票,所以让我们看看是否会再次发生......:)
答案 3 :(得分: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“虚拟”的确如此,我发现wikipedia上的文章很有启发性,尽管那也似乎关注钻石问题
答案 4 :(得分: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