有没有不将成员函数设为虚函数的理由?

25

在C++中,有什么真正的理由将成员函数设置为虚函数吗?当然,在大多数情况下,虚函数的开销相对较低,因此性能问题似乎并不会粘着在程序上。

另一方面,我曾经遇到过几次忘记将本应该是虚函数的函数设置为虚函数的情况。这似乎比性能问题更为重要。所以,默认情况下将成员函数设置为虚函数是否存在任何理由不这样做呢?


虚函数在两个方面破坏性能:一是间接函数调用,正如你所说,并不那么糟糕。二是编译器无法内联函数调用。我有一个案例,在没有虚函数的情况下,GCC 4.3可以构建出比虚函数快10倍的代码。如果它可以内联,那么就可以进行许多其他优化。 - Zan Lynx
[D编程语言][http://digitalmars.com/d/](被称为C++的升级版)默认情况下使每个函数都是虚函数,如果不必要,编译器会进行优化。因此,理论上将来也可以在C++中实现这一点。 - Alexander Malakhov
7个回答

28
一种解读你的问题的方式是:“为什么C++不默认将每个函数设置为虚函数,除非程序员覆盖此默认设置。” 没有查阅我的《C++设计与演化》副本:这样做会使每个类都增加额外的存储空间,除非每个成员函数都被设置为非虚函数。在我看来,这需要编译器实现更多的工作,并降低采用C++的速度,因为它为追求性能的人提供了食物(我算是其中一员)。
另一种解读你的问题的方式是:“为什么C++程序员不将每个函数设置为虚函数,除非他们有非常好的理由?” 性能的借口可能就是原因。根据你的应用程序和领域,这可能是一个好理由或不是。例如,我的团队中的一部分人在市场数据行情站点工作。在单个流上达到100,000条以上的消息/秒,虚函数开销将是不可接受的。我的团队的其他部分涉及复杂的交易基础设施。在这种情况下,将大多数函数设置为虚函数可能是一个好主意,因为额外的灵活性胜过微观优化。

27

语言设计者 Stroustrup :

因为许多类并不是设计成基类来使用的。例如,参见class complex

此外,具有虚函数的类的对象需要虚函数调用机制所需的空间 - 通常每个对象需要一个字。这种开销可能很大,并且可能妨碍与其他语言(如 C 和 Fortran)的数据布局兼容性。

有关更多设计理念,请参阅《C++的设计与演化》。


11

有几个原因。

首先是性能:虚函数的开销相对较低,但它也阻止了编译器进行内联,这是C++中一个巨大的优化源。C++标准库之所以表现出色,是因为它可以内联其中包含的许多小型一行函数。此外,具有虚方法的类不是POD数据类型,因此会受到许多限制。它不能仅通过memcpy复制,构造它更加昂贵,占用更多空间。涉及非POD类型时,很多事情突然变得非法或不那么高效。

其次,符合良好的面向对象编程实践。类的重点在于它进行某种抽象、隐藏其内部细节,并提供保证:“这个类将如此如此地行事,并始终保持这些不变量。它永远不会处于无效状态。”如果允许其他人覆盖任何成员函数,这就非常难以做到。在类中定义的成员函数存在的目的是确保不变量的维护。如果我们不关心这一点,那就可以将内部数据成员设为public,并让人们任意操纵它们。但我们希望我们的类是一致的。这意味着我们必须指定其公共接口的行为。这可能涉及特定的自定义点,例如使单个函数虚拟化,但它几乎总是需要使大多数方法非虚拟的,以便它们可以完成确保不变量的工作。非虚拟接口惯用语就是一个很好的例子: http://www.gotw.ca/publications/mill18.htm

第三,继承通常不是必需的,特别是在C++中。模板和泛型编程(静态多态性)在许多情况下比继承(运行时多态性)更好。是的,有时仍然需要虚方法和继承,但这肯定不是默认选项。如果是这样,你就做错了。与其试图假装它是其他语言,不如与语言一起工作。C++不是Java,与Java不同,在C++中,继承是例外而不是规则。


8
我会忽略性能和内存成本,因为我无法针对“总体”情况进行测量...
具有虚拟成员函数的类是非POD类型。因此,如果您希望在依赖于它作为POD类型的底层代码中使用该类,则(除其他限制外)任何成员函数都必须是非虚拟的。
可以轻松移植 POD 类实例的示例:
- 使用 memcpy 进行复制(前提是目标地址具有足够的对齐度)。 - 使用 offsetof() 访问字段 - 通常将其视为 char 序列 - ... 嗯,大约就是这样。我肯定忘记了一些东西。
其他一些人提到的事情我也同意: - 许多类不适合继承。使它们的方法虚拟化是误导性的,因为它意味着子类可能希望重写该方法,而且不应该有任何子类。 - 许多方法不设计为可被重写:同样的道理。 此外,即使某些内容被打算用来子类化/重写,它们也不一定用于运行时多态性。极少数情况下,尽管面向对象的最佳实践是这样说的,但您希望使用继承来进行代码重用。例如,如果您正在使用 CRTP 进行模拟动态绑定。因此,不要通过使其方法虚拟化来暗示您的类会与运行时多态性相容,因为它们永远不应该以这种方式调用。
总之,打算用于运行时多态性的内容应标记为 virtual,而不是则不需要。如果发现几乎所有成员函数都可以进行虚拟,那么除非有理由不这样做,否则请将它们标记为 virtual。如果发现大多数成员函数都不意图进行虚拟,则不应将它们标记为 virtual,除非有理由这样做。
在设计公共 API 时,这是一个棘手的问题,因为从一种方法切换到另一种方法会导致破坏性变化,因此您必须第一次就正确处理它。但是,在没有任何用户之前,您不一定知道您的用户是否希望“多态”您的类。STL 容器方法是定义抽象接口并完全禁止继承的安全方法,但有时需要用户进行更多输入。

6
以下帖子主要是个人意见,但是内容如下:
面向对象设计包括三个方面,而封装(信息隐藏)是其中之一。如果类的设计在这方面不够稳固,那么其余方面就不会很重要。
曾经有人说过“继承破坏封装”(Alan Snyder '86)。在四人帮设计模式书中有很好的讨论。一个类应该被设计成以一种特定方式支持继承。否则,你将打开继承者滥用的可能性。
我想做一个比喻,即使你的所有方法都是virtual,这就相当于使所有成员都是public。我知道有点牵强,但这就是为什么我使用了“比喻”这个词。

3
在设计类层次结构时,编写一个不应被覆盖的函数可能是有意义的。其中一个例子是如果你正在使用“模板方法”模式,在这种情况下,你有一个公共方法调用几个私有虚拟方法。你不希望派生类覆盖它;每个人都应该使用基本定义。
没有“final”关键字,因此向其他开发人员传达一个方法不应被覆盖的最佳方法是使其非虚拟(除了容易忽略的注释)。
在类级别上,使析构函数非虚拟表示该类不应该被用作基类,例如STL容器。
使方法非虚拟表示如何使用它。

3

NVI只是说明虚函数应该是受保护的。 - Nicola Bonelli

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接