【问题标题】:Is there any reason not to make a member function virtual?有什么理由不让成员函数成为虚拟的吗?
【发布时间】:2010-09-22 11:17:20
【问题描述】:

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

另一方面,我曾多次忘记将一个应该虚拟的函数设为虚拟。这似乎是一个比性能更大的论点。那么有什么理由不让成员函数默认为虚拟呢?

【问题讨论】:

  • 虚拟函数以两种方式破坏性能:一是间接函数调用,正如您所说,它还不错。二、编译器不能内联函数调用。我有一个案例,GCC 4.3 在没有虚拟的情况下构建快 10 倍的代码。如果它可以内联,那么它可以做许多其他选择。
  • [D 编程语言][digitalmars.com/d/](据称是 C++ 升级版)默认情况下使每个函数虚拟化,如果没有必要,编译器会对其进行优化。因此,理论上它可以在未来为 C++ 完成

标签: c++ oop virtual-functions


【解决方案1】:

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

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

【讨论】:

    【解决方案2】:

    Stroustrup,语言的设计者,says

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

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

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

    【讨论】:

      【解决方案3】:

      有几个原因。

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

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

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

      【讨论】:

        【解决方案4】:

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

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

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

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

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

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

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

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

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

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

        【讨论】:

          【解决方案5】:

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

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

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

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

          【讨论】:

            【解决方案6】:

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

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

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

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

            【讨论】:

              【解决方案7】:

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

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

              和 cmets 关于 NVI 的成语:

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

              【讨论】:

              • NVI 只是声明虚函数应该受到保护。
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2011-01-22
              相关资源
              最近更新 更多