C ++支持通过虚拟机制进行动态绑定。但据我所知,虚拟机制是编译器的实现细节,标准只是指定了在特定场景下应该发生的行为。大多数编译器通过虚拟表和虚拟指针实现虚拟机制。是的,我知道这是如何工作的,所以我的问题不是关于虚拟指针和表的实现细节。我的问题是:
sizeof
将是该编译器上的指针(this
内的vptr)的大小,所以给定虚拟ptr和tbl机制本身就是编译器实现,我上面提到的这个陈述总是如此吗?答案 0 :(得分:21)
对象中的vtable指针始终是最有效的。我的编译器用于另一种语言,用于使用对象内指针,原因类似但不再使用:相反,它使用一个单独的数据结构,将对象地址映射到所需的元数据:在我的系统中,这恰好是形状信息供使用由垃圾收集器。
此实现为单个简单对象节省了更多的存储空间,对于具有许多基础的复杂对象更有效,并且对于数组来说非常更高效,因为只需要一个条目数组中所有对象的映射表。我的特定实现也可以找到给定指向对象内部任何点的指针的元数据。
实际查找非常快,存储要求非常适中,因为我使用的是地球上最好的数据结构:Judy数组。
我也知道没有使用除vtable指针之外的任何C ++编译器,但它不是唯一的方法。事实上,具有基础的类的初始化语义使任何实现都变得混乱。这是因为完整类型必须在构造对象时进行跷跷板。作为这些语义的结果,复杂的mixin对象导致生成大量的vtable,大对象和慢对象初始化。这可能不是vtable技术的结果,而是需要盲目地遵循子对象的运行时类型始终是正确的要求。实际上在构造过程中没有充分的理由,因为构造函数不是方法,也不能合理地使用虚拟调度:由于析构函数是真正的方法,因此我不太清楚这种情况。
答案 1 :(得分:7)
据我所知,所有C ++实现都使用vtable指针,尽管在对象中保留一个小类型索引会非常容易(并且可能没有你认为给定的缓存那么糟糕)(1-2) B)随后通过小表查找获取vtable和类型信息。
另一个有趣的方法可能是BIBOP(http://foldoc.org/BIBOP) - 大量的页面 - 尽管它会有C ++的问题。想法:在页面上放置相同类型的对象。通过简单地“关闭”对象指针的不太重要的位来获取指向页面顶部的类型描述符/ vtable的指针。 (当然,对堆栈中的对象不起作用!)
另一种方法是在对象指针本身中编码某些类型标记/索引。例如,如果通过构造将所有对象都对齐为16字节,则可以使用4个LSB在其中放置4位类型标记。 (还不够。)或者(特别是对于嵌入式系统)如果你保证地址中未使用的更高有效位,你可以在那里放置更多的标记位,并使用移位和掩码恢复它们。
虽然这两种方案对于其他语言实现都很有趣(有时也会使用),但它们对C ++来说是个问题。某些C ++语义,例如在(基类)对象构造和销毁期间调用哪些基类虚函数覆盖,驱动您进入一个模型,在该模型中,当您输入基类ctors / dtors时,您修改的对象中存在某些状态。 / p>
您可能会发现我对Microsoft C ++对象模型实现的旧教程很有趣。 http://www.openrce.org/articles/files/jangrayhood.pdf
快乐的黑客攻击!
答案 2 :(得分:5)
我认为现有的编译器没有vptr / vtable以外的方法。实际上,很难找出其他不仅仅是效率低下的东西。
然而,在该方法中,设计权衡仍有相当大的空间。特别是关于如何处理虚拟继承。因此,定义此实现是有意义的。
如果您对此类内容感兴趣,我强烈建议您阅读Inside the C++ Object Model。
sizeof class
取决于编译器。如果您想要可移植代码,请不要做任何假设。
答案 3 :(得分:5)
是否有任何编译器以虚拟指针和虚拟表机制以外的任何其他方式实现虚拟机制?据我所见,最多(阅读g ++,Microsoft visual studio)通过虚拟表,指针机制实现它。实际上还有其他任何编译器实现吗?
我所知道的所有当前编译器都使用vtable机制。
这是一种可能的优化,因为C ++是静态类型检查的。
在一些更动态的语言中,有一个动态搜索基类链,搜索虚拟调用的成员函数的实现,从最派生的类开始宾语。例如,这就是它在原始Smalltalk中的工作方式。 C ++标准描述了虚拟调用的效果,就像使用了这样的搜索一样。
在1990年代的Borland / Turbo Pascal中,这种动态搜索用于查找Windows API“窗口消息”的处理程序。我认为Borland C ++可能也是如此。它是普通vtable机制的补充,仅用于消息处理程序。
如果它在Borland / Turbo C ++中使用 - 我记不起来了 - 那么它支持一种语言扩展,允许你将消息id与消息处理函数相关联。
只有一个虚函数的任何类的sizeof将是该编译器上的指针大小(此内的vptr),因此假设虚拟ptr和tbl机制本身是编译器实现,我上面所做的这个语句总是真?
形式上没有(即使假设vtable机制),它依赖于编译器。由于标准不需要vtable机制,因此它没有说明在每个对象中放置vtable指针。其他规则允许编译器在最后自由添加填充,未使用的字节。
但也许在实践中。 ; - )
然而,这不是你应该依赖的东西,或者你需要依赖的东西。但是在另一个方向,您可以要求,例如,如果您正在定义ABI。那么任何没有的编译器都不符合你的要求。
干杯&第h。,
答案 4 :(得分:4)
在试图想象一个替代方案时,我提出了以下Yttril's answer的方法。据我所知,没有编译器使用它!
给定足够大的虚拟地址空间和灵活的OS内存分配例程,new
可以在固定的,非重叠的地址范围内分配不同类型的对象。然后可以使用右移操作从其地址快速推断出对象的类型,并将结果用于索引vtable表,从而为每个对象保存1个vtable指针。
乍一看,这个方案可能会遇到堆栈分配对象的问题,但这可以干净利落地处理:
(address range, type)
对的全局数组中,并在销毁时删除记录。this
指针的thunk的单个vtable,扫描数组以找到该地址处对象的相应类型(vptr),以及在vtable中调用相应的方法指向。 (即第42个thunk将在vtable中调用第42个方法 - 如果在任何类中使用的大多数虚函数是n
,则至少需要n
个thunk。)对于基于堆栈的对象上的虚方法调用,此方案显然会产生非平凡的开销(至少为查找的O(log n))。在没有基于堆栈的对象的数组或组合(包含在另一个对象中)的情况下,可以使用更简单,更快速的方法,其中vptr紧接在对象之前放置在堆栈上(请注意,它不被视为对象的一部分)对象,并且对sizeof
)测量的大小没有贡献。在这种情况下,thunks只需从sizeof (vptr)
中减去this
以找到要使用的正确vptr,并像以前一样转发。
答案 5 :(得分:4)
IIRC Eiffel使用不同的方法,并且方法的所有覆盖最终在同一地址中合并和编译,其中序列类型被检查(因此每个对象必须具有类型ID,但它不是指向a的指针VMT)。对于C ++,这当然需要在链接时创建最终函数。 但是,我不知道任何使用这种方法的C ++编译器。
答案 6 :(得分:3)
我从来没有听说过或看过任何使用任何替代实现的编译器。 vtable如此受欢迎的原因是因为它不仅是最有效的实现,而且它也是最简单的设计和最明显的实现。
在几乎所有你喜欢使用的编译器上,几乎可以肯定。然而,它并不保证并且并非总是如此 - 你不能依赖它,即使它几乎总是如此。你最喜欢的编译器也可以在不告诉你的情况下改变它的对齐方式,增加它的大小。从内存中,它还可以插入任何调试信息及其喜欢的内容。
答案 7 :(得分:3)
是否有任何编译器以虚拟指针和虚拟表机制以外的任何其他方式实现虚拟机制?据我所见,最多(阅读g ++,Microsoft visual studio)通过虚拟表,指针机制实现它。那么实际上还有其他任何编译器实现吗?
没有我知道C ++编译器正在使用,尽管您可能会发现阅读Binary Tree Dispatch很有意思。如果您有兴趣以任何方式利用虚拟分派表的期望,您应该知道编译器可以 - 在编译时知道类型的位置 - 有时在编译时解析虚函数调用,因此可能不会查询表。
只有一个虚函数的任何类的sizeof将是该编译器上的指针大小(此内的vptr),因此假设虚拟ptr和tbl机制本身是编译器实现,我上面所做的这个语句总是真正?
假设没有基类具有自己的虚拟成员,并且没有虚拟基类,那么它绝对可能是真的。可以设想替代方案 - 例如整个程序分析只显示类heirarchy中的一个成员,以及切换到编译时调度。如果需要运行时调度,很难想象为什么任何编译器会引入进一步的间接寻址。尽管如此,标准仍然没有准确地规定这些事情,以便实现可以变化,或者将来变化。
答案 8 :(得分:3)
C++/CLI偏离这两个假设。如果你定义一个ref类,它根本不会被编译成机器代码;相反,编译器将其编译为.NET托管代码。在中间语言中,类是内置功能,虚拟方法集在元数据中定义,而不是方法表。
实现对象布局和分派的具体策略取决于VM。在Mono中,只包含一个虚方法的对象没有一个指针的大小,但在MonoObject struct中需要两个指针;第二个用于同步对象。由于这是实现定义的,并且也不是很有用,因此C ++ / CLI中的ref类不支持sizeof。
答案 9 :(得分:0)
Tony D's answer正确地指出编译器允许使用整个程序分析来替换虚拟函数调用,并对静态调用唯一可能的函数实现;或者将obj->method()
编译成等效的
if (auto frobj = dynamic_cast<FrequentlyOccurringType>(obj)) {
frobj->FrequentlyOccurringType::method(); // static dispatch on hot path
} else {
obj->method(); // vtable dispatch on cold path
}
Karel Driesen和UrsHölzle在1996年撰写了一篇非常引人入胜的论文,其中他们模拟了完美的整体程序优化对典型C ++应用程序的影响:"The Direct Cost of Virtual Function Calls in C++"。 (如果你是谷歌的话,PDF是免费提供的。)不幸的是,他们只对vtable调度与完美的静态调度进行了基准测试;他们没有将它与二叉树调度进行比较。
当你谈论支持多重继承的语言(如C ++)时,他们 指出实际上有两种vtable。通过多重继承,当您调用从第二个基类继承的虚方法时,您需要“修复”对象指针,使其指向第二个基类的实例。该修正偏移可以作为数据存储在vtable中,或者可以作为代码存储在“thunk”中。 (有关详细信息,请参阅该文章。)
我相信现在所有体面的编译器都会使用thunk,但是这个市场渗透需要10年或20年才能达到100%。
答案 10 :(得分:0)
首先,提到了Borland对C ++,动态调度虚拟表(DDVT)的专有扩展,您可以在名为DDISPATC.ZIP的文件中阅读有关它的内容。 Borland Pascal有virtual和dynamic方法,Delphi introduced yet another "message" syntax类似于动态,但对于消息。在这一点上,我不确定Borland C ++是否具有相同的功能。 Pascal或Delphi中没有多重继承,因此Borland C ++ DDVT可能与Pascal或Delphi不同。
其次,在20世纪90年代和更早的时候,我们尝试了不同的对象模型,而Borland并不是最先进的。我个人认为关闭IBM SOMobjects对我们仍然遭受的世界造成了损害。在关闭SOM之前,有一些使用Direct-to-SOM C ++编译器的实验。因此,使用SOM而不是C ++的调用方法。它在许多方面类似于C ++ vtable,但有几个例外。首先,为了防止脆弱的基类问题,程序不使用vtable中的偏移量,因为它们不知道这个偏移量。如果基类引入了新方法,它可以改变。相反,调用者调用在运行时创建的thunk,其在汇编代码中具有此知识。还有一个区别。在C ++中,当使用多继承时,对象可以包含多个VMT IIRC。与C ++相比,每个SOM对象只有一个VMT,因此调度代码应与&#34;调用dword ptr [VMT + offset]&#34;。
有一个与SOM相关的文档,Release-to-Release Binary Compatibility in SOM。您可以找到SOM与我不了解的其他项目的比较,例如Delta/C++和Sun OBI。它们解决了SOM解决的一部分问题,通过这样做,他们也有一些调整调用代码。
我最近发现Visual Age C ++ v3.5 for Windows编译器片段足以使事情运行并实际触摸它。大多数用户不太可能只使用DTS C ++来获得OS / 2 VM,但拥有Windows编译器完全是另一回事。 VAC v3.5是支持Direct-to-SOM C ++功能的第一个和最后一个版本。 VAC v3.6.5和v4.0不合适。
我以这种方式获得了输出:
Local anInfo->x = 5
Local anInfo->_get_x() = 5
Local anInfo->y = A
Local anInfo->_get_y() = B
{An instance of class info at address 0092E318
}