有没有理由不让会员功能虚拟?

时间:2008-11-15 03:57:12

标签: c++ oop virtual-functions

是否有任何真正的理由在C ++中使成员函数虚拟化?当然,总会存在性能参数,但由于虚函数的开销相当低,因此在大多数情况下似乎并不存在。

另一方面,由于忘记创建应该虚拟的虚拟功能,我被咬了几次。这似乎是一个比表现更大的争论。那么有没有理由不默认将成员函数设为虚拟?

7 个答案:

答案 0 :(得分:23)

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

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

答案 1 :(得分:23)

Stroustrup,该语言的设计者says

  

因为许多类不是设计用作基类。例如,请参阅class complex

     

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

     

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

答案 2 :(得分:10)

有几个原因。

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

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

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

答案 3 :(得分:8)

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

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

您可以使用POD类的实例进行移植的示例:

  • 使用memcpy复制它(假设目标地址有足够的对齐方式)。
  • 使用offsetof()
  • 访问字段
  • 一般来说,将其视为一系列char
  • ......嗯
  • 就是这样。我确定我忘记了什么。

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

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

  • 许多方法都没有被覆盖:同样的事情。

此外,即使打算对事物进行子类化/重写,它们也不一定用于运行时多态性。偶尔,尽管OO最佳实践说,你想继承的是代码重用。例如,如果您使用CRTP进行模拟动态绑定。所以你再次不希望你的类通过使其方法变为虚拟来完美地运行运行时多态性,而不应该以这种方式调用它们。

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

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

答案 4 :(得分:5)

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

面向对象设计是三件事,封装(信息隐藏)是这些事情的第一件事。如果一个班级设计在这方面不是很扎实,那么剩下的并不重要。

之前已经说过“继承打破了封装”(Alan Snyder '86)在四个设计模式的集合中存在一个很好的讨论。应该设计一个类以非常特定的方式支持继承。否则,你就有可能被继承人误用。

我会把所有方法都虚拟化,这类似于让所有成员公开。我知道,这有点延伸,但这就是为什么我使用“类比”这个词

答案 5 :(得分:3)

非虚拟接口习语使用非虚拟方法。有关更多信息,请参阅Herb Sutter“Virtuality”文章。

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 [见小节]

答案 6 :(得分:2)

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

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

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

使非虚拟方法告知如何使用它。