24

有什么真正的理由不在C++ 中将成员函数设为虚拟吗?当然,总是有性能争论,但在大多数情况下似乎并不成立,因为虚函数的开销相当低。

另一方面,我已经被咬过几次忘记将一个应该是虚拟的函数虚拟化。这似乎是一个比性能更大的论点。那么有什么理由不让成员函数默认为虚拟的吗?

4

7 回答 7

27

阅读您的问题的一种方法是“为什么 C++ 默认情况下不使每个函数都成为虚拟函数,除非程序员覆盖该默认值。” 无需咨询我的“C++ 的设计和演变”副本:这将为每个类增加额外的存储空间,除非每个成员函数都被设为非虚拟。在我看来,这将需要在编译器实现方面付出更多努力,并通过为痴迷于性能的人提供素材来减缓 C++ 的采用(我认为自己属于那个群体。)

阅读您的问题的另一种方法是“为什么 C++ 程序员不使每个函数都成为虚拟函数,除非他们有充分的理由不这样做?” 性能借口可能是原因。根据您的应用程序和域,这可能是一个很好的理由,也可能不是。例如,我团队的一部分人在市场数据行情公司工作。在单个流上每秒超过 100,000 条消息时,虚函数开销将是不可接受的。我团队的其他部分在复杂的交易基础设施中工作。在这种情况下,使大多数功能虚拟化可能是一个好主意,因为额外的灵活性胜过微优化。

于 2008-11-15T04:26:52.513 回答
27

该语言的设计者 Stroustrup

因为许多类并非设计为用作基类。例如,请参阅类 complex

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

有关更多设计原理,请参阅 C++ 的设计和演变。

于 2008-11-15T04:29:56.643 回答
11

有几个原因。

首先,性能:是的,单独来看,虚函数的开销相对较低。但它也阻止了编译器内联,这是 C++ 中优化的一个巨大来源。C++ 标准库的性能和它一样好,因为它可以内联它所包含的几十个和几十个小的单行代码。此外,具有虚拟方法的类不是 POD 数据类型,因此有很多限制适用于它。它不能仅仅通过 memcpy'ing 来复制,它会变得更昂贵,并且占用更多空间。一旦涉及到非 POD 类型,很多事情就会突然变得非法或效率降低。

其次,良好的 OOP 实践。类的重点在于它进行了某种抽象,隐藏了其内部细节,并提供了保证“这个类会表现得如此如此,并且将始终保持这些不变量。它永远不会处于无效状态” . 如果您允许其他人覆盖任何成员函数,这将很难实现。您在类中定义的成员函数用于确保保持不变式。如果我们不关心这一点,我们可以将内部数据成员公开,让人们随意操纵它们。但是我们希望我们的类是一致的。这意味着我们必须指定其公共接口的行为。这可能涉及特定的可定制性点,通过使单个函数成为虚拟的,但它几乎总是涉及使大多数方法成为非虚拟的,以便它们可以完成确保保持不变性的工作。非虚拟界面习语就是一个很好的例子: http ://www.gotw.ca/publications/mill18.htm

第三,通常不需要继承,尤其是在 C++ 中。在许多情况下,模板和泛型编程(静态多态)比继承(运行时多态)做得更好。是的,你有时仍然需要虚拟方法和继承,但它肯定不是默认的。如果是,那么你做错了。使用语言,而不是试图假装它是别的东西。它不是 Java,而且与 Java 不同,在 C++ 中继承是例外,而不是规则。

于 2008-11-15T13:59:56.697 回答
8

我将忽略性能和内存成本,因为我无法在“一般”情况下测量它们......

具有虚拟成员函数的类是非 POD。因此,如果您想在依赖于 POD 的低级代码中使用您的类,那么(除其他限制外)任何成员函数都必须是非虚拟的。

您可以使用 POD 类的实例可移植地做的事情的示例:

  • 用 memcpy 复制它(前提是目标地址有足够的对齐)。
  • 使用 offsetof() 访问字段
  • 通常,将其视为 char 序列
  • ……嗯
  • 就是这样。我确定我忘记了什么。

人们提到的其他我同意的事情:

  • 许多类不是为继承而设计的。将他们的方法设为虚拟会产生误导,因为这意味着子类可能想要覆盖该方法,并且不应该有任何子类。

  • 许多方法并非旨在被覆盖:同样的事情。

此外,即使打算对事物进行子类化/覆盖,它们也不一定用于运行时多态性。偶尔,尽管 OO 最佳实践说了什么,但您想要继承的是代码重用。例如,如果您使用 CRTP 进行模拟动态绑定。因此,您再次不想暗示您的类将通过将其方法设置为虚拟来很好地使用运行时多态性,而这些方法绝不应该以这种方式调用。

总之,旨在为运行时多态性覆盖的东西应该被标记为虚拟的,而不应该被标记的东西。如果您发现几乎所有的成员函数都是虚拟的,那么除非有理由不这样做,否则将它们标记为虚拟。如果您发现您的大多数成员函数都不是虚拟的,那么不要将它们标记为虚拟,除非有这样做的理由。

在设计公共 API 时,这是一个棘手的问题,因为将一个方法从一个转换到另一个是一个破坏性的变化,所以你必须在第一时间把它做好。但是在拥有任何用户之前,您不一定知道您的用户是否想要“变形”您的类。哼哼。定义抽象接口和完全禁止继承的 STL 容器方法是安全的,但有时需要用户进行更多输入。

于 2008-11-15T13:20:24.623 回答
6

以下帖子主要是意见,但这里有:

面向对象设计是三件事,封装(信息隐藏)是其中的第一件事。如果一个类设计在这方面不够扎实,那么剩下的就不是很重要了。

之前已经说过“继承破坏封装”(Alan Snyder '86),关于这一点的很好的讨论出现在四人组设计模式书中。一个类应该被设计成以一种非常具体的方式支持继承。否则,您就有可能被继承人滥用。

我会打个比方,让你所有的方法都是虚拟的,就像让你的所有成员都公开一样。有点牵强,我知道,但这就是我使用“类比”这个词的原因

于 2008-11-15T04:27:39.160 回答
3

在设计类层次结构时,编写一个不应被覆盖的函数可能是有意义的。一个例子是,如果您正在执行“模板方法”模式,其中您有一个调用多个私有虚拟方法的公共方法。您不希望派生类覆盖它;每个人都应该使用基本定义。

没有“final”关键字,因此与其他开发人员交流不应覆盖方法的最佳方式是使其成为非虚拟方法。(除了容易被忽略的评论)

在类级别,使析构函数为非虚拟表示该类不应用作基类,例如 STL 容器。

使方法成为非虚拟方法可以传达应该如何使用它。

于 2008-11-15T05:03:09.837 回答
3

Non-Virtual Interface 习惯用法使用非虚拟方法。欲了解更多信息,请参阅 Herb Sutter“虚拟”文章。

http://www.gotw.ca/publications/mill18.htm

以及对 NVI 成语的评论:

http://www.parashift.com/c++-faq-lite/strange-inheritance.html#faq-23.3 http://accu.org/index.php/journals/269 [见小节]

于 2008-11-15T05:44:50.580 回答