关于c ++:有没有理由不让成员函数虚拟化?

Is there any reason not to make a member function virtual?

在C++中有没有使成员函数虚拟化的真正原因?当然,性能参数总是存在的,但由于虚拟函数的开销相当低,所以在大多数情况下,这似乎并不适用。

另一方面,我已经被咬了几次,忘记让一个应该是虚拟的函数成为虚拟的。这似乎是一个比性能更重要的论点。那么,有什么理由不让成员函数在默认情况下成为虚拟的呢?


该语言的设计者stroustrup说:

Because many classes are not designed to be used as base classes. For example, see class complex.

Also, objects of a class with a virtual function require space needed by the virtual function call mechanism - typically one word per object. This overhead can be significant, and can get in the way of layout compatibility with data from other languages (e.g. C and Fortran).

See The Design and Evolution of C++ for more design rationale.


阅读你的问题的一个方法是"为什么C++不让每个函数默认为虚拟的,除非程序员重写默认的",而不必查阅我的"C++的设计和演化"的副本:除非每个成员函数都是非虚的,否则这会给每个类增加额外的存储空间。在我看来,这将需要更多的努力在编译器实现,并减缓了采用C++提供饲料的性能痴迷(我算我自己在该组)。

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


有几个原因。

首先,性能:是的,虚拟函数的开销相对较低。但它也阻止编译器内联,这是C++优化的一个巨大来源。C++标准库执行得和它一样,因为它可以内嵌它所组成的几十个和几十个小的一个内衬。另外,具有虚方法的类不是pod数据类型,因此有很多限制。它不可能仅仅通过内存复制,建造成本更高,占用的空间也更大。一旦涉及到非pod类型,很多事情会突然变得非法或效率低下。

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

第三,继承不是经常需要的,特别是在C++中。模板和通用编程(静态多态性)在许多情况下可以比继承(运行时多态性)做得更好。是的,有时您仍然需要虚拟方法和继承,但它肯定不是默认的。如果是的话,那就是你做错了。使用这种语言,而不是假装它是其他东西。它不是Java,而不像Java,在C++中继承是例外,而不是规则。


我将忽略性能和内存成本,因为我没有办法衡量它们的"一般"情况…

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

使用pod类的实例可以方便地执行的操作示例:

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

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

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

  • 许多方法设计为不被重写:同一件事。

而且,即使当事物被设计成子类/重写时,它们也不一定是为运行时多态性设计的。偶尔,尽管OO最佳实践中说了什么,但您希望继承的目的是代码重用。例如,如果您使用CRTP进行模拟动态绑定。因此,您也不想暗示您的类将通过使其方法成为虚拟的来很好地处理运行时多态性,而不应该以这种方式调用它们。

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

在设计公共API时,这是一个棘手的问题,因为将一个方法从一个方法翻转到另一个方法是一个突破性的变化,所以您必须第一次正确地处理它。但是在你有任何用户之前,你不一定知道你的用户是否想要"变形"你的类。嗬哼。STL容器方法定义抽象接口并完全禁止继承,是安全的,但有时需要用户进行更多的输入。


下面的文章主要是观点,但这里是:

面向对象设计是三件事,封装(信息隐藏)是第一件事。如果一个类的设计在这方面不可靠,那么其他的就不太重要了。

前面已经说过"继承破坏了封装"(AlanSynyder'86),在《四个设计模式》一书中对这一点进行了很好的讨论。类的设计应该以非常具体的方式支持继承。否则,您就有可能被继承者滥用。

我可以做一个类比,让你所有的方法都虚拟化,就像让你所有的成员都公开一样。我知道有点夸张,但这就是为什么我用"类比"这个词的原因。


非虚拟接口习惯用法使用非虚拟方法。有关更多信息,请参阅Herb Sutter"虚拟"一文。

http://www.gotw.ca/publications/mill18.htm(网址:www.gotw.ca/publications/mill18.htm)

以及对nvi成语的评论:

http://www. PARASIFIF.COM/C++-FAQLIT/陌生继承。http://accu.org/index.php/journals/269[见小节]


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

没有"final"关键字,因此与其他开发人员通信的最佳方式是使方法非虚拟化,而不应重写该方法。(除了容易忽略的评论)

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

使一个方法非虚拟地传达它应该如何使用。