为什么虚拟function不能过度使用?

我刚刚读到我们不应该过度使用虚函数。 人们认为虚拟function越少,错误越少,维护也越少。

由于虚函数会出现什么样的缺陷和缺点?

我对C ++或Java的上下文感兴趣。


我能想到的一个原因是由于v表查找,虚函数可能比正常函数慢。

你发表了一些一揽子声明,我认为大多数务实的程序员会因为误导或误解而耸耸肩。 但是,确实存在反虚拟狂热者,他们的代码对于性能和维护也同样糟糕。

在Java中,默认情况下一切都是虚拟的。 说你不应该过度使用虚函数非常强大。

在C ++中,您必须声明一个虚拟函数,但在适当的时候使用它们是完全可以接受的。

我刚刚读到我们不应该过度使用虚函数。

很难定义“过度”……当然“在适当的时候使用虚拟function”是一个很好的建议。

人们认为虚拟function越少,错误越少,维护也越少。 由于虚函数,我无法获得可能出现的错误和缺点。

设计不良的代码很难维护。 期。

如果你是一个库维护者,调试隐藏在高级层次结构中的代码,可能很难跟踪代码实际执行的位置,而没有强大的IDE的好处,通常很难判断哪个类覆盖了这个行为。 它可以导致在跟踪inheritance树的文件之间跳转很多。

所以,有一些经验法则,都有例外:

  • 保持层次结构浅薄。 高大的树木令人困惑。
  • 在c ++中,如果你的类有虚函数,使用虚拟析构函数(如果没有,它可能是一个bug)
  • 与任何层次结构一样,保持派生类和基类之间的“is-a”关系。
  • 你必须要知道,根本不能调用虚函数…所以不要添加隐含的期望。
  • 有一个难以争辩的案例,虚拟function较慢。 它是动态绑定的,所以情况经常如此。 在大多数案例中是否重要,其引用肯定是有争议的。 简介和优化:)
  • 在C ++中,不要在不需要时使用虚拟。 标记虚拟function涉及语义 – 不要滥用它。 让读者知道“是的,这可能会被覆盖!”。
  • 首选纯虚拟接口到混合实现的层次结构。 它更清晰,更容易理解。

这种情况的实际情况是虚拟function非常有用,而且这些疑问不太可能来自平衡源 – 虚拟function已经被广泛使用了很长时间。 更新的语言正在采用它们作为默认语言。

虚函数比常规函数稍慢。 但是这种差异是如此之小,以至于除了最极端的情况外,并没有产生任何影响。

我认为避免虚拟function的最佳理由是防止接口滥用。

编写可以扩展的类是一个好主意,但是有太多开放的东西。 通过仔细规划哪些function是虚拟的,您可以控制(并保护)如何扩展类。

扩展类时会出现错误和维护问题,从而破坏基类的合同。 这是一个例子:

class Widget { private WidgetThing _thing; public virtual void Initialize() { _thing = new WidgetThing(); } } class DoubleWidget : Widget { private WidgetThing _double; public override void Initialize() { // Whoops! Forgot to call base.Initalize() _double = new WidgetThing(); } } 

这里,DoubleWidget打破了父类,因为Widget._thing为null。 有一种相当标准的方法来解决这个问题:

 class Widget { private WidgetThing _thing; public void Initialize() { _thing = new WidgetThing(); OnInitialize(); } protected virtual void OnInitialize() { } } class DoubleWidget : Widget { private WidgetThing _double; protected override void OnInitialize() { _double = new WidgetThing(); } } 

现在,Widget稍后不会遇到NullReferenceException

每个依赖项都会增加代码的复杂性,并使维护更加困难。 当您将函数定义为虚拟时,可以在其他代码上创建类的依赖关系,此时可能甚至不存在。

例如,在C中,您可以轻松找到foo()的作用 – 只有一个foo()。 在没有虚函数的C ++中,它稍微复杂一些:你需要探索你的类及其基类来找到我们需要的foo()。 但至少你可以提前确定,而不是在运行时。 使用虚函数,我们无法分辨执行哪个foo(),因为它可以在子类中定义。

(另一件事是你提到的性能问题,由于v-table)。

我怀疑你误解了这个说法。

过分是一个非常主观的术语,我认为在这种情况下,它意味着“当你不需要它时”,而不是你应该避免使用它。

根据我的经验,一些学生,当他们了解虚拟function并且第一次因忘记虚拟function而被烧毁时, 认为简单地使每个function都是虚拟的是谨慎的

由于虚函数确实会在每次方法调用上产生成本(在C ++中通常不会因为单独的编译而避免),因此您实际上是为每个方法调用付费并且还防止内联。 许多教师不鼓励学生这样做,尽管术语“过度”是一个非常糟糕的选择。

在Java中,“虚拟”行为(动态调度)是默认行为。 但是,JVM可以动态地优化事物,理论上可以在目标身份清除时消除一些虚拟调用。 另外,最终类中的最终方法或方法通常也可以在编译时解析为单个目标。

在C ++中: –

  1. 虚函数有轻微的性能损失。 通常它太小而不能产生任何差别,但在紧密的循环中它可能是重要的。

  2. 虚函数通过一个指针增加每个对象的大小。 这通常是微不足道的,但如果你创造了数以百万计的小物件,它可能是一个因素。

  3. 具有虚函数的类通常意味着inheritance自。 派生类可以替换部分,全部或不替换虚函数。 这可能会造成额外的复杂性和复杂性,是程序员的致命敌人。 例如,派生类可能很难实现虚函数。 这可能会破坏依赖虚函数的基类的一部分。

现在让我说清楚:我不是说“不要使用虚函数”。 它们是C ++的重要组成部分。 请注意复杂性的可能性。

我们最近有一个完美的例子,说明虚拟函数的滥用如何引入错误。

有一个具有消息处理程序的共享库:

 class CMessageHandler { public: virtual void OnException( std::exception& e ); ///other irrelevant stuff }; 

意图是您可以从该类inheritance并将其用于自定义error handling:

 class YourMessageHandler : public CMessageHandler { public: virtual void OnException( std::exception& e ) { //custom reaction here } }; 

error handling机制使用CMessageHandler*指针,因此它不关心对象的实际类型。 该函数是虚函数,因此只要存在重载版本,就会调用后者。

很酷,对吗? 是的,直到共享库的开发人员更改了基类:

 class CMessageHandler { public: virtual void OnException( const std::exception& e ); //<-- notice const here ///other irrelevant stuff }; 

......而且重载只是停止了工作。

你看到发生了什么? 更改基类后,从C ++的角度来看,重载已停止为重载 - 它们成为新的,其他不相关的函数

基类的默认实现未标记为纯虚拟,因此派生类不会强制重载默认实现。 最后,只有在error handling的情况下调用function,并不是每次都使用。 所以这个bug被默默地引入并且在相当长的一段时间内被忽视了。

一劳永逸地消除它的唯一方法是搜索所有代码库并编辑所有相关的代码片段。

我不知道你在哪里阅读,但imho这根本不是关于性能的。

也许更多的是关于“更喜欢inheritance的组合”以及如果你的类/方法不是最终的(在这里我主要谈论java)而不是真正设计用于重用的问题。 有很多事情可能会出错:

  • 也许你在构造函数中使用虚方法 – 一旦被覆盖,你的基类就会调用重写方法,该方法可能使用在子类构造函数中初始化的ressources – 后来运行(NPE上升)。

  • 想象一下列表类中的add和addAll方法。 addAll调用添加很多次,都是虚拟的。 有人可能会覆盖它们以计算已添加的项目数量。 如果你没有记录addAll调用add,开发人员可能(并将会)覆盖add和addAll(并向他们添加一些反向++东西)。 但是现在,如果你使用addAll,每个项目都会计数两次(add和addAll),这会导致错误的结果并且很难找到错误。

总结一下,如果你没有设计你的类进行扩展(提供钩子,记录一些重要的实现事项),你根本不允许inheritance,因为这可能导致平均错误。 如果需要的话,也很容易从你的一个类中删除一个final修饰符(也许可以重新设计它的可重用性),但它不可能使非final类(子类化导致错误)最终,因为其他人可能已经将它子类化了。

也许它真的是关于性能,然后我至少是关于主题。 但如果它不是,那么你有一些很好的理由不让你的类可扩展,如果你真的不需要它。

关于Blochs Effective Java中有关类似内容的更多信息(这篇特别的post是在我阅读第16项(“更喜欢构图而不是inheritance”)和17(“设计和文档inheritance或禁止它”)后几天写的) – 很棒的书。

在大约7年的时间里,我偶尔在同一个C ++系统上担任顾问,检查了大约4-5名程序员的工作。 每次我回去,系统都变得越来越糟。 在某些时候,有人决定删除所有虚拟function,并用一个非常钝的工厂/基于RTTI的系统替换它们,这些系统基本上完成了虚拟function已经在做的所有事情但是更糟糕,更昂贵,数千行代码,大量工作,大量的测试,……完全无懈可击,显然是对未知驱动的恐惧。

当编译器自动生成错误时,他们还手工编写了数十个带有错误的复制构造函数,没有错误,只有三个例外情况需要手写版本。

道德:不要与语言作斗争。 它给你的东西:使用它们。

为每个类创建虚拟表,具有虚函数或从包含虚函数的类派生。 这比通常的空间消耗更多。

编译器需要静默插入额外的代码,以确保发生后期绑定而不是早期绑定。 这比平时消耗更多。