【发布时间】:2011-03-06 03:25:50
【问题描述】:
我读过一些模糊的说法,virtual inheritance 没有提供 COM 所需的内存结构,所以我们必须使用正常的继承。发明虚拟继承是为了处理钻石问题。
谁能告诉我这两种继承方法之间内存结构细节的差异?以及虚拟继承不适合 COM 的关键原因。最好有图片。
非常感谢。
【问题讨论】:
我读过一些模糊的说法,virtual inheritance 没有提供 COM 所需的内存结构,所以我们必须使用正常的继承。发明虚拟继承是为了处理钻石问题。
谁能告诉我这两种继承方法之间内存结构细节的差异?以及虚拟继承不适合 COM 的关键原因。最好有图片。
非常感谢。
【问题讨论】:
首先,在 COM 中总是使用虚拟继承的行为。 QueryInterface 不能返回不同的值,例如IUnknown 基指针取决于用于获取它的派生类。
但你说得对,这与 C++ 中的虚拟继承机制不同。 C++ 不使用QueryInterface 函数进行向上转换,因此需要另一种获取基类指针的方法。
内存布局问题是因为 COM 要求基接口的所有方法都可以直接使用派生接口指针调用。 AddRef 就是一个很好的例子。在 COM 中,您可以调用AddRef 并将任何派生接口作为this 指针传递。在 C++ 中,AddRef 实现将期望 this 指针的类型为 IUnknown* const。不同之处在于,在 C++ 中,调用者找到基指针,而在 COM 中,被调用者进行调整以找到基指针,因此每个派生接口都需要一个不同的实现(至少是 QueryInterface)知道从派生接口指针传入基指针。
乍一看,作为实现细节,C++ 编译器可以选择让被调用者像 COM 一样执行调整。但是指向成员函数的指针规则与虚拟基类的这种实现不兼容。
【讨论】:
COM coclass 可以实现多个接口,但每个单独的接口都必须实现一个 v-table,其中包含指向其“基本”接口引入的所有方法的指针。至少 IUnknown。例如,如果它实现了 IPersistFile,那么它必须提供三个 IUnknown 方法以及 IPersist::GetClassID 的实现。以及 IPersistFile 特定的方法。
这恰好与大多数 C++ 编译器在实现非虚拟多重继承时的行为相匹配。编译器为每个继承的(纯抽象)类设置单独的 v-table。并用方法指针填充它,以便一个公共类方法实现接口共有的所有方法。换句话说,无论实现了多少接口,它们都由一个类方法提供服务,例如 QueryInterface、AddRef 或 Release。
正是您希望它工作的方式。拥有 AddRef/Release 的一种实现使引用计数变得简单,以保持 coclass 对象处于活动状态,无论您分发多少不同的接口指针。 QueryInterface 实现起来很简单,一个简单的转换提供了指向具有正确布局的 v-table 的接口指针。
不需要虚拟继承。并且很可能会破坏 COM,因为 v-tables 不再具有所需的布局。这对于任何编译器来说都是棘手的,例如查看 MSVC 编译器的 /vm 选项。 COM 如此不可思议地与 C++ 编译器的典型行为兼容,这不是意外。
顺便说一句,当一个 coclass 想要实现多个接口,这些接口有一个共同的方法名称,而这并不意味着做同样的事情时,这一切都让粉丝们大吃一惊。这是一个相当大的哎呀,很难处理。在 ATL Internals (DAdvise?) 中提到,我很遗憾忘记了解决方案。
【讨论】:
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 来查找这些成员是不同的B-within-D 或 B-within-something-else。编译器需要一些运行时线索(虚拟表)来查找从 A 继承的成员。您最终需要几个指向 D 实例中多个虚拟表的指针,因为继承的 B 和继承的 C 等都有一个 vtable,这意味着一些内存开销。
单继承不存在这些问题。实例的内存布局保持简单,虚拟表也更简单。这就是 Java 不允许类的多重继承的原因。在接口继承中没有数据成员,所以这些问题根本不会出现 - 没有 which-inherited-A-with-D 的问题,也没有根据特定 B 找到 A-within-B 的不同方法的问题恰好在里面。 COM 和 Java 都可以允许接口的多重继承,而不必处理这些复杂问题。
编辑
我忘了说 - 没有数据成员,虚拟和非虚拟继承之间没有真正的区别。然而,在 Visual C++ 中,即使没有数据成员,布局也可能有所不同 - 无论是否存在任何数据成员,对每种继承样式都始终使用相同的规则。
此外,COM 内存布局与 Visual-C++ 布局(对于支持的继承类型)相匹配,因为它就是为此而设计的。没有理由不能将 COM 设计为支持具有数据成员的“接口”的多重和虚拟继承。 Microsoft 本可以将 COM 设计为支持与 C++ 相同的继承模型,但选择不这样做 - 而且他们没有理由不这样做。
早期的 COM 代码通常是用 C 编写的,这意味着必须与 Visual-C++ 布局精确匹配才能工作的手写结构布局。多重和虚拟继承的布局——好吧,我不会自愿手动做。此外,COM 始终是它自己的东西,旨在链接用多种不同语言编写的代码。它从未打算与 C++ 绑定。
还有更多编辑
我意识到我错过了一个关键点。
在 COM 中,唯一重要的布局问题是虚拟表,它只需要处理方法分派。布局有显着差异,具体取决于您采用虚拟还是非虚拟方法,类似于具有数据成员的对象的布局...
对于接口继承,这基本上是实现细节 - A 只有一组方法实现。
在非虚拟情况下,A 虚拟表的两个副本将是相同的(导致相同的方法实现)。它是一个稍大的虚拟表,但每个对象的开销更少,并且指针转换只是类型重新标记(没有地址更改)。它的实现更简单、更高效。
COM 无法检测到虚拟案例,因为对象或 vtable 中没有指示符。此外,当没有数据成员时,支持这两种约定是没有意义的。它只支持一种简单的约定。
【讨论】:
struct D : B 和 struct D : virtual B 的布局肯定不一样。我看不出他们怎么能有类似的布局,而不是非常低效。