繁体   English   中英

为什么不将 C++ 中的所有功能都设为虚拟?

[英]Why not have all the functions as virtual in C++?

我知道虚函数有取消引用来调用方法的开销。 但我想以现代建筑速度几乎可以忽略不计。

  1. C++ 中的所有功能都不像 Java 中那样虚拟,有什么特别的原因吗?
  2. 据我所知,在基础 class 中定义虚拟 function 就足够/必要了。 现在,当我编写父 class 时,我可能不知道哪些方法会被覆盖。 这是否意味着在编写子 class 时,有人必须编辑父 class。 这听起来很不方便,有时是不可能的?

更新:
总结以下 Jon Skeet 的回答:

这是在明确让某人意识到他们正在继承功能[这本身具有潜在风险[(查看 Jon 的回复)] [和潜在的小幅性能提升]之间的权衡取舍,以换取更少的灵活性、更多的代码更改,以及更陡峭的学习曲线。

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

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

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

控制哪些方法在性能之外是虚拟的,这是有充分理由的。 虽然我实际上并没有在 Java 中最终确定我的大部分方法,但我可能应该......除非一个方法被设计为被覆盖,它可能不应该是虚拟 IMO。

为 inheritance 进行设计可能会很棘手 - 特别是这意味着您需要记录更多关于它的名称和名称的信息。 想象一下,如果您有两个虚拟方法,一个调用另一个 -必须记录在案,否则有人可以用调用“调用”方法的实现覆盖“调用”方法,无意中创建堆栈溢出(或无限循环,如果有尾调用优化)。 那时,您在实施中的灵活性就会降低 - 您不能在以后切换它。

请注意,C# 在各种方面与 Java 是相似的语言,但默认情况下选择将方法设为非虚拟。 其他一些人对此并不热衷,但我当然欢迎它——而且我实际上更希望类在默认情况下也是不可继承的。

基本上,这归结为 Josh Bloch 的建议:为 inheritance 设计或禁止它。

  1. C++ 的主要原则之一是:您只需为使用的内容付费(“零开销原则”)。 如果您不需要动态调度机制,则不应为其开销买单。

  2. 作为基础 class 的作者,您应该决定应该允许覆盖哪些方法。 如果您同时编写两者,请提前 go 并重构您需要的内容。 但它是这样工作的,因为基础 class 的作者必须有一种方法来控制它的使用。

但我想以现代建筑速度几乎可以忽略不计。

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

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

虚函数也是如此。 我们之前也有过类似的讨论; 例如, 是否有任何理由使用 C++ 代替 C、Perl、ZA7F5F35426B9278417139?

大多数答案都涉及虚拟功能的开销,但是还有其他原因不在 class 中创建任何function 虚拟,因为它会将 class 从标准布局更改为非标准布局如果您需要序列化二进制数据,这可能是一个问题。 这在 C# 中以不同的方式解决,例如,通过使struct s 是与class es 不同的类型族。

从设计的角度来看,每个公共 function 在您的类型和该类型的用户之间建立一个契约,每个虚拟 function(公共或非公共)与扩展您的类型的类建立不同的契约。 您签署的此类合同数量越多,您拥有的更改空间就越小。 事实上,有不少人,包括一些著名的作家,捍卫公共接口不应该包含虚函数,因为您对客户的妥协可能与您对扩展的要求不同。 也就是说,公共界面显示您为客户所做的事情,而虚拟界面显示其他人如何帮助您完成它。

虚函数的另一个影响是它们总是被分派给最终的覆盖器(除非您明确限定调用),这意味着维护您的不变量所需的任何 function(认为私有变量的 state)不应该是虚拟的:如果 class 对其进行了扩展,则必须对父级进行显式合格的回调,否则会破坏您级别的不变量。

这类似于@Jon Skeet 提到的无限循环/堆栈溢出的示例,只是方式不同:您必须在每个 function 中记录它是否访问任何私有属性,以便扩展确保 function 在适当的时候。 这反过来意味着您正在破坏封装并且您有一个泄漏的抽象:您的内部细节现在是接口的一部分(文档 + 对您的扩展的要求),您不能随意修改它们。

然后是性能...会对性能产生影响,但在大多数情况下,这被高估了,并且可以说,只有在性能至关重要的少数情况下,您才会退回并声明函数为非虚拟的. 再说一次,这在构建产品上可能并不简单,因为两个接口(公共 + 扩展)已经绑定。

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

我在这里参加聚会已经很晚了,所以我将添加一件事,我没有注意到其他答案中涵盖的内容,并快速总结...

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

  • 封装:class 设计器控制客户端代码访问的成员的能力,确保维护 class 语义和不变量。 例如,如果您从std::string派生(我可能会因为敢于建议而得到一些评论;-P),那么您可以使用所有正常的插入/擦除/append 操作并确保 - 只要您不这样做对std::string执行任何始终未定义的行为,例如将错误的 position 值传递给函数 - std::string数据将是正确的。 检查或维护您的代码的人不必检查您是否更改了这些操作的含义。 对于 class,封装可确保以后在不破坏客户端代码的情况下自由修改实现。 同一陈述的另一个观点:客户端代码可以以任何喜欢的方式使用 class,而无需对实现细节敏感。 如果任何 function 可以在派生的 class 中更改,那么整个封装机制就会被吹走。

    • 隐藏的依赖关系:当您既不知道哪些其他功能依赖于您要覆盖的功能,也不知道 function 被设计为被覆盖,那么您就无法推断更改的影响。 例如,您认为“我一直想要这个”,并更改std::string::operator[]()at()以将负值(在类型转换为有符号之后)从字符串的结尾。 但是,也许其他一些 function 正在使用at()作为一种索引有效的断言 - 知道它会否则抛出 - 在尝试插入或删除之前......该代码可能 go 以标准指定的方式抛出有未定义的(但可能是致命的)行为。
    • 文档:通过制作 function virtual ,您正在记录它是一个预期的定制点,并且是 API 的一部分供客户端代码使用。

  • 内联- 代码端和 CPU 使用情况:虚拟分派使编译器确定何时内联 function 调用的工作变得复杂,因此可能会在空间/膨胀和 CPU 使用方面提供更差的代码。

  • 调用期间的间接调用:即使以任何一种方式进行了离线调用,虚拟调度的性能成本也会很小,当在性能关键系统中重复调用非常简单的函数时,这可能会非常显着。 (您必须读取指向虚拟调度表的每个对象的指针,然后读取虚拟调度表条目本身——这意味着 VDT 页面也在消耗缓存。)

  • Memory 用法:指向虚拟调度表的每个对象的指针可能表示严重浪费了 memory,尤其是对于小对象的 arrays。 这意味着更少的对象适合缓存,并可能对性能产生重大影响。

  • Memory 布局:C++ 可以使用精确的 memory 布局和各种网络指定的成员数据的协议数据标准定义类,这对于性能至关重要,并且对于互操作性非常方便。 该数据通常来自您的 C++ 程序之外,并且可能以另一种语言生成。 这样的通信和存储协议对于指向虚拟调度表的指针不会有“间隙”,正如前面所讨论的那样——即使它们确实存在,并且编译器以某种方式让您有效地为您的进程注入正确的指针,而不是传入数据,这会令人沮丧多进程访问数据。 基于粗略但实用的指针/大小的序列化/反序列化/通信代码也将变得更加复杂并且可能更慢。

按使用付费(用 Bjarne Stroustrup 的话)。

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

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

在 Java 中,所有方法都是虚拟的,并且还存在开销。 但是,与 C++ 不同,JIT 编译器在运行时分析代码,并且可以内联那些不使用此属性的方法。 因此,JVM 知道哪里真正需要它,哪里不需要,从而使您无需自己做出决定。

问题是,虽然 Java 编译为在虚拟机上运行的代码,但不能为 C++ 做出同样的保证。 通常使用 C++ 作为 C 的更有条理的替代品,并且 C 具有 1:1 的汇编转换。

如果您认为世界上 10 个微处理器中有 9 个不在个人计算机或智能手机中,那么当您进一步考虑有很多处理器需要这种低级访问时,您就会发现问题所在。

C++ 旨在避免在不需要时进行隐藏的尊重,从而保持 1:1 的性质。 一些最初的 C++ 代码实际上有一个中间步骤,即在运行 C 到汇编编译器之前将其转换为 C。

由于运行时优化,Java 方法调用比 C++ 高效得多。

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM