我读过一些模糊的陈述,virtual inheritance没有提供COM所需的内存结构,所以我们必须使用普通继承。发明虚拟继承来处理钻石问题。
有人能告诉我这两种继承方法之间存储结构细节差异的说明吗? 关键原因为什么虚拟继承不适合COM。一张照片最好。
非常感谢。
答案 0 :(得分:4)
首先,在COM中始终使用虚拟继承的行为。 QueryInterface
无法为例如{0}返回不同的值IUnknown
基指针取决于用于获取它的派生类。
但你是对的,这与C ++中的虚拟继承不同。 C ++不使用QueryInterface
函数进行upcast,因此它需要另一种获取基类指针的方法。
引起内存布局问题是因为COM要求可以使用派生接口指针直接调用基接口的所有方法。 AddRef
就是一个很好的例子。在COM中,您可以调用AddRef
并将任何派生接口作为this
指针传递。在C ++中,AddRef
实现期望this指针的类型为IUnknown* const
。区别在于,在C ++中,调用者找到基指针,而在COM中,被调用者进行调整以找到基指针,因此每个派生接口都需要一个独特的实现(至少QueryInterface
)知道传递给基指针的派生接口指针的偏移量。
乍一看,C ++编译器可以选择让被调用者像COM一样执行调整作为实现细节。但是指向成员函数的指针规则与虚拟基类的这种实现不兼容。
答案 1 :(得分:4)
COM coclass可以实现多个接口,但每个单独的接口必须实现一个v-table,其中包含指向所有其“基础”接口引入的方法的指针。至少IUnknown。如果它实现了IPersistFile,那么它必须提供三个IUnknown方法以及IPersist :: GetClassID的实现。和IPersistFile的具体方法。
这恰好与大多数C ++编译器实现非虚拟多重继承时的行为相匹配。编译器为每个继承的(纯抽象)类设置单独的v表。并使用方法指针填充它,以便一个公共类方法实现接口共有的所有方法。换句话说,无论实现了多少接口,它们都由QueryInterface,AddRef或Release等一类方法提供服务。
正是你希望它的工作方式。使用AddRef / Release的一个实现使得引用计数变得简单,以保持coclass对象的活动,无论您分发多少个不同的接口指针。 QueryInterface实现起来很简单,一个简单的转换提供了一个具有正确布局的v表的接口指针。
不需要虚拟继承。并且很可能会破坏COM,因为v表不再具有所需的布局。这对于任何编译器来说都很棘手,例如,查看MSVC编译器的/ vm选项。那个COM如此不可思议地与C ++编译器的典型行为兼容是而不是是一个意外。
顺便说一下,当一个coclass想要实现多个具有共同方法名称但并不意味着做同样事情的接口时,这一切都会击中粉丝。这是一个非常大的哎呀,很难处理。在ATL Internals(DAdvise?)中提到过,我遗憾地忘记了解决方案。
答案 2 :(得分:2)
COM接口在某种程度上与JAVA接口相似 - 它们没有数据成员。这意味着当使用多重继承时,接口继承与类继承不同。
首先,考虑使用菱形继承模式的非虚拟继承......
D的实例包含A的数据成员的两个独立实例。这意味着当指向A的指针指向D的实例时,它需要识别D中哪个A实例意味着 - 指针在每种情况下都是不同的,并且指针强制转换不是类型的简单重新标记 - 地址也会改变。
现在考虑具有虚拟继承的相同钻石。 B,C和D的实例都包含单个A实例。如果您认为B和C具有固定布局(包括A实例),则这是一个问题。如果Bs布局是[A,x]并且Cs布局是[A,y],那么[B,C,z]对D无效 - 它将包含两个A实例。你需要使用的是类似于[ A,B',C',z]其中B'是B的所有东西,除了继承的A等。
这意味着如果你有一个指向B的指针,你就没有一个解除引用从A继承的成员的方案。根据指针是指向一个纯粹的B还是一个成员,这些成员是不同的B-within-D或B-within-something-else。编译器需要一些运行时线索(虚拟表)来查找从A成员继承的成员。你最终需要指向D实例中几个虚拟表的几个指针,因为它们是继承的B和继承的C等的vtable,这意味着一些内存开销。
单继承没有这些问题。实例的内存布局保持简单,虚拟表也更简单。这就是为什么Java不允许类的多重继承。在接口继承中没有数据成员,所以再次出现这些问题根本不会出现 - 没有哪个继承-A-with-D的问题,也没有找到A-within-B的不同方法,具体取决于特定的B碰巧在。 COM和Java都可以允许多个接口继承,而无需处理这些复杂情况。
修改强>
我忘了说 - 没有数据成员,虚拟和非虚拟继承之间没有真正的区别。但是,使用Visual C ++,即使没有数据成员,布局也可能不同 - 对每种继承样式使用相同的规则,无论是否存在任何数据成员。
此外,COM内存布局与Visual-C ++布局(对于支持的继承类型)匹配,因为它是为此而设计的。没有理由不能将COM设计为支持与数据成员的“接口”的多个和虚拟继承。微软本可以设计COM来支持与C ++相同的继承模型,但选择不这样做 - 并且他们没有理由不这样做。
早期的COM代码通常用C语言编写,这意味着手写的结构布局必须与Visual-C ++布局精确匹配才能工作。多重和虚拟继承的布局 - 好吧,我不会自愿手动完成。此外,COM总是它自己的东西,意味着链接用许多不同语言编写的代码。它从来没有打算与C ++联系在一起。
YET MORE EDITING
我意识到我错过了一个关键点。
在COM中,唯一重要的布局问题是虚拟表,它只需处理方法调度。布局存在显着差异,具体取决于您采用虚拟还是非虚拟方法,类似于具有数据成员的on对象布局......
使用接口继承,这基本上是实现细节 - A只有一组方法实现。
在非虚拟情况下,A虚拟表的两个副本将是相同的(导致相同的方法实现)。它是一个稍微大一点的虚拟表,但每个对象的开销较少,指针转换只是类型重新标记(没有地址更改)。它的实现更简单,更有效。
COM无法检测虚拟案例,因为对象或vtable中没有指示符。此外,当没有数据成员时,没有必要支持这两种约定。它只支持一个简单的约定。