为什么不把所有的function都用C ++虚拟化?

我知道虚拟函数有一个解引用的开销来调用一个方法。 但我认为现代build筑的速度几乎可以忽略不计。

  1. C ++中的所有函数都不像Java一样是虚拟的吗?
  2. 据我所知,在基类中定义一个虚函数是充分/必要的。 现在当我写一个父类时,我可能不知道哪个方法会被覆盖。 那么这是否意味着在编写子类时,有人将不得不编辑父类。 这听起来不方便,有时不可能?

更新:
总结从Jon Skeet的答案如下:

在明确地让某人意识到他们正在inheritancefunction(这对他们自己有潜在的风险[(Jon的回答检查)] [和潜在的小的性能收益]之间进行权衡时,需要权衡灵活性,代码更改和更陡峭的学习曲线。

来自不同答案的其他原因:

虚拟函数不能被插入,因为内联必须在运行时发生。 当您希望函数从内联中获益时,这会对性能产生影响。

可能还有其他原因,我很想知道并总结它们。

有很好的理由控制哪些方法是虚拟的超越性能。 虽然我实际上并没有把我的大部分方法都做成Java的最终版本,但是我应该…除非一个方法被devise为被覆盖,否则它可能不应该是虚拟的IMO。

deviseinheritance可能会非常棘手 – 特别是它意味着您需要更多地logging可能调用它的内容以及可能调用的内容。 想象一下,如果你有两个虚拟方法,一个调用另一个 – 必须logging下来,否则有人可以用一个调用“调用”方法的实现覆盖“调用”方法,无意中创build一个堆栈溢出(或无限循环,如果尾部呼叫优化)。 到那时,你的实现灵活性就会降低 – 以后你就不能轮换。

请注意,C#是一种与Java相似的语言,但是默认情况下select使得方法是非虚拟的。 其他一些人并不热衷于此,但是我当然欢迎它 – 而且我更喜欢默认的课程也是不可行的。

基本上,这归结于Josh Bloch的这个build议:deviseinheritance或禁止它。

  1. 其中一个主要的C ++原则是:你只支付你使用的东西(“零开销原则”)。 如果你不需要dynamic调度机制,你不应该支付它的开销。

  2. 作为基类的作者,你应该决定哪些方法应该被允许被覆盖。 如果你正在写这两个,请继续并重构你所需要的。 但是,它是这样工作的,因为基类的作者必须有一种方法来控制它的使用。

但我认为现代build筑的速度几乎可以忽略不计。

这个假设是错误的,而且我猜这个决定的主要原因。

考虑内联的情况。 在某些情况下,C ++的sort函数比C的类似的qsort 快得多,因为它可以内联比较器参数,而C不能(由于使用函数指针)。 在极端情况下,这可能意味着高达700%的性能差异(Scott Meyers,Effective STL)。

虚拟function也是如此。 我们之前也有类似的讨论 例如, 是否有任何理由使用C ++而不是C,Perl,Python等?

大多数答案都是关于虚拟函数的开销,但是还有其他一些原因,使得虚拟类中没有任何function,因为它会将类从标准布局改变为非标准布局 ,如果你需要序列化二进制数据可能是一个问题。 这在C#中的解决方法是不同的,例如,通过使struct s是与class es不同的types族。

从devise的angular度来看,每一个公共函数在你的types和types的用户之间build立了一个契约,每一个虚拟函数(公共的或不是公有的)与扩展你的types的类别build立了一个不同的契约。 您签署的合同数量越多,您所拥有的变更空间就越小。 事实上,包括一些知名作家在内的不less人,都认为公共接口永远不应该包含虚拟function,因为你对客户的妥协可能与你的扩展需要的妥协不同。 也就是说,公共接口显示了你为你的客户所做的事情,而虚拟接口显示了其他人可以帮你做的事情。

虚函数的另一个作用是它们总是被派发到最后的覆盖(除非你明确地限定了调用),这意味着任何维护你的不variables所需要的函数(认为私有variables的状态)不应该是虚拟的:如果一个类扩展它,它将不得不向父对象进行明确的合格调用,否则将打破你的级别的不variables。

这与@Jon Skeet提到的无限循环/堆栈溢出的例子类似,只是以不同的方式:必须在每个函数中logging它是否访问任何私有属性,以便扩展将确保函数在合适的时间。 而这又意味着你正在破解封装,并且你有一个泄漏的抽象:你的内部细节现在是界面的一部分(文档+你的扩展的需求),你不能随意修改它们。

然后就是performance……performance会有影响,但是在大多数情况下,这是被高估的,可以说只有在less数performance关键的情况下,你会退缩,宣布这些function是非虚拟的。 然后再次,这可能不是一个简单的build筑产品,因为两个接口(公共+扩展)已经绑定。

你忘了一件事 开销也在内存中,那就是为每个对象添加一个虚拟表和一个指向该表的指针。 现在如果你有一个对象有大量的实例,那么它是不可忽略的。 例如,百万个实例等于4个兆字节。 我同意,对于简单的应用程序来说,这不算什么,但是对于实时设备,如路由器这很重要。

按使用付费 (以Bjarne Stroustrup文字)。

我在这里参加晚会比较晚,所以我会补充一点我没有注意到的其他答案,然后快速总结。

  • 共享内存中的可用性 :虚拟调度的典型实现具有指向每个对象中的特定于类的虚拟调度表的指针。 这些指针中的地址是特定于创build它们的进程的,这意味着访问共享内存中的对象的多进程系统不能使用另一个进程的对象进行调度! 鉴于共享内存在高性能多进程系统中的重要性,这是一个不可接受的限制。

  • 封装(Encapsulation) :类devise者控制客户端代码所访问的成员的能力,确保类的语义和不variables得以保持。 例如,如果你从std::string派生(我可能会得到一些敢于提示的注释; -P),那么你可以使用所有正常的插入/擦除/追加操作,并确保 – 只要你不对std::string做任何总是未定义的行为,比如将不好的位置值传递给函数 – std::string数据将是正确的。 有人检查或维护你的代码,不必检查你是否改变了这些操作的含义。 对于一个类来说,封装确保以后修改实现而不中断客户端代码的自由。 同样的声明的另一个观点:客户端代码可以以任何方式使用类,而不会对实现细节敏感。 如果任何函数都可以在派生类中更改,那么整个封装机制就会被简单地吹走。

    • 隐藏的依赖关系 :当你既不知道其他函数依赖于你所压倒的函数,也不知道函数是否被devise为被覆盖的时候,那么你就不能推断出你的改变的影响。 例如,你认为“我一直想要这个”,并改变std::string::operator[]()at()认为负值(在types转换为有符号之后)从string的结尾。 但是,也许有一些其他函数使用at()作为一种断言,索引是有效的 – 知道它会抛出否则 – 在试图插入或删除之前…该代码可能会抛出在标准指定的方式有未定义的(但可能是致命的)行为。
    • 文档 :通过virtualfunction,您logging了这是定制的一个预期点,并且是客户端代码使用的API的一部分。
  • 内联 – 代码端和CPU使用情况:虚拟调度使编译器的工作变得复杂化,以便在内联函数调用时工作,因此可以在空间/膨胀和CPU使用方面提供更差的代码。

  • 在呼叫期间的间接性 :即使是以任一方式进行非线性呼叫,对于虚拟调度而言,在性能关键系统中重复调用简单的简单函数时,性能成本可能很小。 (您必须读取每个对象指向虚拟分派表的指针,然后虚拟分派表条目本身 – 意味着VDT页面也正在使用高速caching。)

  • 内存使用情况 :指向虚拟分派表的每个对象指针可能代表大量浪费的内存,特别是对于小对象数组。 这意味着更less的对象适合caching,并且可能会对性能产生重大影响。

  • 内存布局 :性能必不可less,互操作性极高,C ++可以根据networking指定的成员数据的确切内存布局或各种库和协议的数据标准来定义类。 这些数据通常来自您的C ++程序之外,并可能以其他语言生成。 这样的通信和存储协议对于虚拟分派表的指针并不存在“间隙”,正如前面所讨论的那样 – 即使他们这样做了,而且编译器以某种方式让你有效地在进程数据上注入正确的指针,多进程访问数据。 粗糙但实际的基于指针/尺寸的序列化/反序列化/通信代码也将变得更复杂并且可能更慢。

似乎这个问题可能有一些答案虚拟function不应该过度使用 – 为什么? 。 在我看来,突出的一点是,它只是增加了更多的复杂性,知道什么可以做的inheritance。

是的,这是因为性能开销。 使用虚拟表和间接方式调用虚拟方法。

在Java中,所有的方法都是虚拟的,开销也是存在的。 但是,与C ++相反,JIT编译器会在运行时对代码进行剖析,并且可以将那些不使用此属性的方法排成一行。 所以,JVM知道它真正需要的地方,哪里不能让你自己做出决定。

问题在于,当Java编译为在虚拟机上运行的代码时,对C ++不能做出同样的保证。 通常使用C ++作为C的更有组织的替代品,并且C具有1:1的程序集转换。

如果考虑到全球10个微处理器中有9个不在个人电脑或智能手机中,那么当您进一步考虑有大量处理器需要这种低级访问时,就会看到这个问题。

C ++的devise是为了避免隐藏的参考,如果你不需要它,从而保持1:1的本质。 一些第一个C ++代码实际上有一个中间步骤,在通过C到汇编编译器运行之前被转换成C语言。

由于运行时优化,Java方法调用比C ++更有效率。

我们需要的是将C ++编译成字节码并在JVM上运行。