【问题标题】:Why can't we use "virtual inheritance" in COM?为什么我们不能在 COM 中使用“虚拟继承”?
【发布时间】:2011-03-06 03:25:50
【问题描述】:

我读过一些模糊的说法,virtual inheritance 没有提供 COM 所需的内存结构,所以我们必须使用正常的继承。发明虚拟继承是为了处理钻石问题

谁能告诉我这两种继承方法之间内存结构细节的差异?以及虚拟继承不适合 COM 的关键原因。最好有图片。

非常感谢。

【问题讨论】:

    标签: c++ com


    【解决方案1】:

    首先,在 COM 中总是使用虚拟继承的行为。 QueryInterface 不能返回不同的值,例如IUnknown 基指针取决于用于获取它的派生类。

    但你说得对,这与 C++ 中的虚拟继承机制不同。 C++ 不使用QueryInterface 函数进行向上转换,因此需要另一种获取基类指针的方法。

    内存布局问题是因为 COM 要求基接口的所有方法都可以直接使用派生接口指针调用。 AddRef 就是一个很好的例子。在 COM 中,您可以调用AddRef 并将任何派生接口作为this 指针传递。在 C++ 中,AddRef 实现将期望 this 指针的类型为 IUnknown* const。不同之处在于,在 C++ 中,调用者找到基指针,而在 COM 中,被调用者进行调整以找到基指针,因此每个派生接口都需要一个不同的实现(至少是 QueryInterface)知道从派生接口指针传入基指针。

    乍一看,作为实现细节,C++ 编译器可以选择让被调用者像 COM 一样执行调整。但是指向成员函数的指针规则与虚拟基类的这种实现不兼容。

    【讨论】:

      【解决方案2】:

      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?) 中提到,我很遗憾忘记了解决方案。

      【讨论】:

      • +1。另外值得注意的是,消费者只能使用他们请求的界面的指定布局,仅此而已。有时,如果您运行的是单一公寓并同时控制 COM 对象和使用者,那么作弊可能很诱人。但是,跨公寓/DCOM 调用将不起作用,并且会严重失败。受封送处理的接口实际上是远程对象的代理,远程对象几乎从不指向原始提供者对象。
      【解决方案3】:

      COM 接口在某种程度上很像 JAVA 接口——它们没有数据成员。这意味着当使用多重继承时,接口继承与类继承是不同的。

      首先,考虑使用菱形继承模式的非虚拟继承...

      • B 继承 A
      • C 继承 A
      • D 继承 B 和 C

      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 中,唯一重要的布局问题是虚拟表,它只需要处理方法分派。布局有显着差异,具体取决于您采用虚拟还是非虚拟方法,类似于具有数据成员的对象的布局...

      • 对于非虚拟,D vtab 包含 A-within-B vtab 和 A-within-C vtab。
      • 对于虚拟,A 在 Ds vtable 中仅出现一次,但对象包含多个 vtable,并且指针转换需要更改地址。

      对于接口继承,这基本上是实现细节 - A 只有一组方法实现。

      在非虚拟情况下,A 虚拟表的两个副本将是相同的(导致相同的方法实现)。它是一个稍大的虚拟表,但每个对象的开销更少,并且指针转换只是类型重新标记(没有地址更改)。它的实现更简单、更高效。

      COM 无法检测到虚拟案例,因为对象或 vtable 中没有指示符。此外,当没有数据成员时,支持这两种约定是没有意义的。它只支持一种简单的约定。

      【讨论】:

      • 我对这个答案的接受感到非常不舒服 - 我该如何把它拿走? - 我通过思考回答,从继续的事情中编辑,这里有一些东西可能解释了为什么在 COM 早期发生这种情况的一个关键方面,但我对 COM 的理解非常有限和过时,而且这个答案根本不准确。在 DCOM 中,它根本不可能是正确的,即使在它只是 OLE2 后端的时代,QueryInterface 的实现原则上也可以做任何它喜欢的事情。
      • "单继承没有这些问题。" 显然,单继承只有一个共享的vptr和vtable。 “在接口继承中没有数据成员,所以这些问题根本不会出现”如果“这些问题”是指您之前对“您最终需要几个指向多个虚拟表的指针”的描述D 实例”和“暗示一些内存开销”那么这显然是错误的:如果你想保持 C++ 虚函数的运行时效率,接口的 MI 意味着与继承的接口一样多的 vptr 和 vtables,在 C++ 或 Java 中来电。
      • "我忘了说 - 没有数据成员,虚拟和非虚拟继承之间没有真正的区别。" 绝对不正确。恐怕您根本不了解 C++ 中的继承。 “但是,对于 Visual C++,即使没有数据成员,布局也可能不同struct D : Bstruct D : virtual B 的布局肯定不一样。我看不出他们怎么能有类似的布局,而不是非常低效。
      • "对于virtual,A在Ds vtable中只出现一次,但是对象包含多个vtable,并且指针转换需要改变地址。" 对于非virtual MI,有D中还有多个vptr,一个用于D,与B共享,与B::A共享,一个与C共享,与C::A共享,显然还有多个vtable:一个用于D,扩展并因此与格式兼容B 的虚表,A 的虚表扩展,C 的虚表(在 D 中)扩展,因此与 A 虚表的格式兼容。
      • 所以D有5个vtables:D中的B::A,D中的B,D,D中的C::A,D中的C。但是前3个和后2个是聚合的: B 中的函数的虚函数指针不覆盖 A 中的函数出现在 A 的 vtable 的末尾,而 D 中的函数不是 ... B 的 vtable。你看到 MI 语义是对称的:所有直接基类具有相同的作用,除了初始化是从左到右完成(并以相反的顺序进行破坏),但 MI 实现是不对称的:D 的 vtable 由 B 的扩展 vtable 组成,但 C 的 vtable 未在 D 中扩展.
      猜你喜欢
      • 1970-01-01
      • 2011-08-24
      • 2010-09-22
      • 1970-01-01
      • 2010-09-28
      • 1970-01-01
      • 2011-08-20
      • 1970-01-01
      • 2013-12-18
      相关资源
      最近更新 更多