可选的虚拟机制实现?

C ++通过虚拟机制支持dynamic绑定。 但据我所知,虚拟机制是编译器的实现细节,标准只是指定了在特定场景下应该发生的行为。 大多数编译器通过虚拟表和虚拟指针来实现虚拟机制。 是的,我知道这是如何工作的,所以我的问题不是关于虚拟指针和表的实现细节。 我的问题是:

  1. 除虚拟指针和虚拟表机制之外,是否有任何其他方式实现虚拟机制的编译器? 据我所见(阅读g ++,Microsoft Visual Studio)通过虚拟表,指针机制来实现它。 那么实际上是否还有其他编译器实现呢?
  2. 任何具有虚函数的类的sizeof将是该编译器中的一个指针(vptr)的大小。因此,假设虚拟ptr和tbl机制本身就是编译器实现,那么我上面所做的这个陈述总是正确的吗?

对象中的vtable指针总是最有效的。 我的另一种语言的编译器用于使用对象内指针的原因类似的原因,但不再:它使用一个单独的数据结构,将对象地址映射到所需的元数据:在我的系统中,这恰好是使用的形状信息由垃圾收集器。

这个实现为单个简单对象花费更多的存储空间,对于有许多基础的复杂对象来说效率更高,对于数组而言,这对于数组来说效率要高得多 ,因为在映射表中只需要一个单独的条目来存放数组中的所有对象。 我的特定实现还可以find元数据给出一个指向该对象内部任何点的指针。

实际查找速度非常快,存储要求非常适中,因为我正在使用这个星球上最好的数据结构:Judy数组。

我也知道没有任何C ++编译器使用vtable指针以外的任何东西,但这不是唯一的方法。 实际上,基类的初始化语义使得任何实现都变得混乱。 这是因为完整的types必须在构build对象时才能看到。 作为这些语义的结果,复杂的mixin对象会导致生成大量的vtables集合,大对象和缓慢的对象初始化。 这可能不是vtable技术的结果,而是需要随意地遵循子对象的运行时types始终正确的要求。 实际上在施工过程中没有什么好的理由,因为构造函数不是方法,也不能明智地使用虚拟调度:由于析构函数是真正的方法,所以对于我来说这不是很清楚。

据我所知,所有的C ++实现都使用了一个vtable指针,虽然它可能很容易(也许不像你认为的caching那么糟糕)来在对象(1-2 B)中保留一个小型索引随后通过小表查找来获得vtable和types信息。

另一个有趣的方法可能是BIBOP(http://foldoc.org/BIBOP); – 大量的页面 – 虽然它会有C ++的问题。 build议:在页面上放置相同types的对象。 获取一个指向页面顶部的types描述符/ vtable的指针,只需简单地closures对象指针的较不重要的位。 (当然,对于堆叠中的物体效果不佳!)

另一种方法是在对象指针本身中编码某些types的标签/索引。 例如,如果通过构造所有对象都是16字节alignment的,则可以使用4个LSB将4位types的标记放在那里。 (还不够。)或者(特别是对于embedded式系统),如果你已经保证了地址中没有使用的更重要的位,你可以把更多的标记位放在那里,然后用shift和mask来恢复它们。

虽然这两种scheme对于其他语言实现都很有趣(有时也被使用),但它们对于C ++来说是有问题的。 某些C ++语义(例如在(基类)对象构造和销毁期间调用哪些基类虚函数覆盖)将驱使您进入一个模型,在您input基类ctors / dtors时修改该对象中的某些状态。

你可能会发现我的旧教程在Microsoft C ++对象模型实现上很有趣。 articles/files/jangrayhood.pdf

快乐的黑客!

除虚拟指针和虚拟表机制之外,是否有任何其他方式实现虚拟机制的编译器? 据我所见(阅读g ++,Microsoft Visual Studio)通过虚拟表,指针机制来实现它。 那么实际上是否还有其他编译器实现呢?

我所知道的所有当前编译器都使用了vtable机制。

这是一个可能的优化,因为C ++是静态types检查。

在一些更dynamic的语言中,相反,dynamicsearch基类链,search虚拟调用的成员函数的实现,从对象的最派生类开始。 例如,这就是它在原来的Smalltalk中的工作原理。 而C ++标准描述了虚拟呼叫的效果, 就好像这样的search一样。

在1990年代的Borland / Turbo Pascal中,这种dynamicsearch被用于寻找Windows API“窗口消息”的处理程序。 我想在Borland C ++中可能是一样的。 除了正常的vtable机制之外,它仅用于消息处理程序。

如果它在Borland / Turbo C ++中使用 – 我不记得 – 那么它支持语言扩展,允许您将消息ID与消息处理函数相关联。

任何具有虚函数的类的sizeof将是该编译器中的一个指针的大小(vptr)。因此,假设虚拟ptr和tbl机制本身是编译器实现的,那么我上面所做的这个陈述总是正确的吗?

正式地否(即使假设vtable机制),它依赖于编译器。 由于标准不需要vtable机制,所以在每个对象中都没有提到vtable指针的位置。 而其他规则让编译器自由地添加填充,未使用的字节,最后。

但实际上也许。 😉

但是,这不是你应该依赖的,或者你需要依赖的。 但是在另一个方向上,你可以要求这个,例如,如果你正在定义一个ABI。 那么任何不是的编译器,都不符合你的要求。

干杯&hth。,

  1. 我不认为有任何现代编译器与vptr / vtable以外的方法。 事实上,很难找出其他不仅仅是低效率的东西。

    但是,在这种方法中,devise权衡还有很大的空间。 特别是关于如何处理虚拟inheritance。 因此,使这个实现定义是有意义的。

    如果你对这类东西感兴趣,我强烈build议阅读Inside the C ++ Object Model 。

  2. sizeof class取决于编译器。 如果你想要可移植的代码,不要做任何假设。

在试图想象一个替代scheme时,我根据伊特里尔的回答提出了以下几点。 据我所知,没有编译器使用它!

给定一个足够大的虚拟地址空间和灵活的操作系统内存分配例程, new程序可以在固定的,不重叠的地址范围内分配不同types的对象。 然后,使用右移操作可以快速从其地址推断出对象的types ,并将结果用于索引vtable表,从而为每个对象节省1个vtable指针。

乍一看,这个scheme可能会遇到堆栈分配对象的问题,但是这可以干净地处理:

  1. 对于每个堆栈分配的对象,编译器添加代码,以便在创build对象时将一条logging添加到全局数组(address range, type)对中,并在logging被销毁时将其删除。
  2. 包含堆栈的地址范围将映射到一个包含大量thunk的vtable,读取this指针,扫描数组以find该地址对象的对应types(vptr),然后调用vtable中相应的方法指出。 (即第42 thunk将在vtable中调用第42个方法 – 如果在任何类中使用的大多数虚函数是n ,那么至less需要n thunk。)

这种scheme显然会导致在基于堆栈的对象上进行虚拟方法调用的不小的开销(至lessO(log n)用于查找)。 在没有基于堆栈的对象的数组或组合(包含在另一个对象中)的情况下,可以使用更简单和更快速的方法,其中vptr紧挨着对象放置在堆栈上(注意,它不被认为是对象,并没有对sizeof大小作出贡献)。 在这种情况下, sizeof (vptr)简单地从this减去sizeof (vptr)来find正确的vptr使用,并像以前一样前进。

  1. 我从来没有听说过或看到任何使用任何替代实现的编译器。 vtable之所以如此受欢迎,是因为它不仅是最有效的实现,而且也是最简单的devise和最明显的实现。

  2. 在几乎所有你喜欢使用的编译器上,几乎肯定是这样的。 然而,这并不是保证,也并非总是正确的 – 即使情况总是如此,你也不能依靠它。 你最喜欢的编译器也可以改变它的alignment方式,增加它的大小,对于funsies,没有告诉你。 从内存中,它也可以插入任何debugging信息和任何喜欢的东西。

除虚拟指针和虚拟表机制之外,是否有任何其他方式实现虚拟机制的编译器? 据我所见(阅读g ++,Microsoft Visual Studio)通过虚拟表,指针机制来实现它。 那么实际上是否还有其他编译器实现呢?

没有我知道的C ++编译器使用,但您可能会发现阅读二叉树调度有趣。 如果您对以任何方式利用虚拟调度表的期望感兴趣,您应该知道,编译器可以在编译时知道哪些types,有时可以在编译时parsing虚拟函数调用,因此可能不会查询表。

任何具有虚函数的类的sizeof将是该编译器中的一个指针的大小(vptr)。因此,假设虚拟ptr和tbl机制本身是编译器实现的,那么我上面所做的这个陈述总是正确的吗?

假设没有自己的虚拟成员的基类,也没有虚拟的基类,这是绝对可能的。 可以设想替代scheme – 例如整个程序分析只能揭示class级中的一名成员,并转换成编译时间调度。 如果需要运行时调度,很难想象为什么编译器会引入更多的间接性。 但是,“标准”并没有精确地规定这些事情,因此实施可能会有所不同,或者将来会有所不同。

C ++ / CLI偏离了这两个假设。 如果你定义了一个ref类,它根本不会被编译成机器代码; 相反,编译器将其编译为.NET托pipe代码。 在中间语言中,类是一个内置的特性,在元数据中定义了一组虚拟方法,而不是方法表。

实现对象布局和调度的具体策略依赖于虚拟机。 在Mono中,一个只包含一个虚拟方法的对象没有一个指针的大小,但是在MonoObject结构中需要两个指针; 第二个用于对象的同步。 由于这是实现定义的,也并不是真正有用的,C ++ / CLI中的ref类不支持sizeof。

IIRC Eiffel使用了一种不同的方法,一个方法的所有覆盖最终被合并和编译在同一个地址中,在这个地址中检查对象types(所以每个对象都必须有一个typesID,但不是指向VMT的指针)。 这对于C ++来说当然需要在链接时创build最终的函数。 但是,我不知道使用这种方法的任何C ++编译器。

Tony D的答案正确地指出,编译器允许使用整个程序分析来用唯一可能的函数实现的静态调用来代替虚拟函数调用; 或者将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 ++应用程序中模拟了完美的整体程序优化的效果: “虚拟函数在C ++中的直接代价” 。 (如果你是Google的话,这个PDF是免费的。)不幸的是,他们只testing了vtable调度和完美的静态调度。 他们没有把它比作二叉树调度。

他们指出,实际上有两种vtable,当你谈论支持多inheritance的语言(如C ++)时。 对于多重inheritance,当调用从第二个基类inheritance的虚方法时,需要“修正”对象指针,使其指向第二个基类的实例。 这个修正偏移量可以作为数据存储在vtable中,也可以作为代码存储在“thunk”中。 (更多细节见文件)

我相信这些日子所有优秀的编译器都使用thunk,但是为​​了达到100%的市场渗透需要10到20年的时间。

首先,提到了Borland对C ++dynamic调度虚拟表(DDVT)的专有扩展,您可以在一个名为DDISPATC.ZIP的文件中阅读关于它的内容 。 Borland Pascal拥有虚拟和dynamic两种方法,而Delphi引入了另外一种类似于dynamic的“消息”语法 ,但是对于消息。 在这一点上,我不确定Borland C ++是否具有相同的function。 在Pascal或Delphi中都没有多重inheritance,所以Borland C ++ DDVT可能不同于Pascal或Delphi。

其次,在20世纪90年代和稍早的时候,他们尝试了不同的对象模型,而Borland并不是最先进的。 我个人认为,closuresIBM SOMobjects对我们仍然患有的世界造成了损害。 在closuresSOM之前,曾经有过使用Direct-to-SOM C ++编译器的实验。 所以,而不是C ++调用方法SOM的方式。 它在很多方面与C ++ vtable相似,只是有一些例外。 首先,为了防止脆弱的基类问题,程序不使用vtable内部的偏移量,因为他们不知道这个偏移量。 如果基类引入新的方法,它可以改变。 相反,调用者调用在运行时创build的thunk,在汇编代码中有这方面的知识。 还有一个区别。 在C ++中,当使用多inheritance时,一个对象可以包含多个VMT IIRC。 与C ++相比,每个SOM对象只有一个VMT,所以调度代码应该与“call dword ptr [VMT + offset]”不同。

在SOM中有一个与SOM相关的文档,即Release-Release-Binary Compatibility 。 你可以findSOM与我所知道的其他项目(比如Delta / C ++和Sun OBI)的比较 。 他们解决了SOM解决的一些问题,通过这样做,他们也调整了一些调用代码。

我最近发现Windows编译器片段的Visual Age C ++ v3.5足以让事情运行并实际触及它。 大多数用户不太可能为了使用DTS C ++而使用OS / 2虚拟机,但使用Windows编译器则完全是另一回事。 VAC v3.5是支持Direct-to-SOM C ++function的第一个和最后一个版本。 VAC v3.6.5和v4.0不合适。

  1. 从IBM FTP下载VAC 3.5 fixpak 9 。 这个fixpak包含许多文件,所以你甚至不需要完整的编译器(我有3.5.7发行版,但fixpak 9足够大,做一些testing)。
  2. 解压到例如C:\ home \ OCTAGRAM \ DTS
  3. 启动命令行并在那里运行后续命令
  4. 运行:set SOMBASE = C:\ home \ OCTAGRAM \ DTS \ ibmcppw
  5. 运行:C:\ home \ OCTAGRAM \ DTS \ ibmcppw \ bin \ SOMENV.BAT
  6. 运行:cd C:\ home \ OCTAGRAM \ DTS \ ibmcppw \ samples \ compiler \ dts
  7. 运行:nmake clean
  8. 运行:nmake
  9. hhmain.exe和它的dll在不同的目录下,所以我们必须让它们以某种方式find对方; 因为我正在做几个实验,所以我执行了一次“set PATH =%PATH%; C:\ home \ OCTAGRAM \ DTS \ ibmcppw \ samples \ compiler \ dts \ xhmain \ dtsdll”,但是您可以将dll复制到hhmain附近。可执行程序
  10. 运行:hhmain.exe

我有这样的输出:

 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 }