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

时间:2010-06-16 04:57:30

标签: java c++ virtual-functions

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

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

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


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

10 个答案:

答案 0 :(得分:14)

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

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

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

  

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

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

  

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

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

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

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

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

情况的实际情况是虚拟功能非常有用,而且这些疑问不太可能来自平衡源 - 虚拟功能已经被广泛使用了很长时间。更多新语言将其作为默认语言而不是其他语言。

答案 1 :(得分:7)

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

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

将类打开以进行扩展是个好主意,但是太开放这样的事情。通过仔细规划哪些功能是虚拟的,您可以控制(并保护)如何扩展类。

当扩展类以破坏基类的契约时,会出现错误和维护问题。这是一个例子:

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为空。有一种相当标准的方法来解决这个问题:

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

答案 2 :(得分:6)

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

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

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

答案 3 :(得分:3)

我怀疑你误解了这句话。

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

根据我的经验,一些学生,当他们了解虚拟功能并且第一次因忘记虚拟功能而被烧伤时,认为简单地使每个功能虚拟是明智的。

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

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

答案 4 :(得分:2)

在C ++中: -

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

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

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

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

答案 5 :(得分:2)

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

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

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

意图是您可以从该类继承并将其用于自定义错误处理:

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

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

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

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

...而且重载已停止工作。

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

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

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

答案 6 :(得分:1)

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

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

  • 也许您在自己的网站中使用虚拟方法 构造函数 - 一旦被覆盖, 你的基类调用被覆盖的 方法,可以使用资源 在子类中初始化 构造函数 - 后来运行(NPE上升)。

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

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

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

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

答案 7 :(得分:0)

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

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

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

答案 8 :(得分:0)

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

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

答案 9 :(得分:0)

在Java中,没有virtual关键字,但是所有方法(函数)都是虚拟的,除了标记为final,静态方法和私有实例方法的方法(函数)之外。使用虚拟函数根本不是一个坏习惯,但是由于通常无法在编译时对其进行解析,并且编译器无法对其进行优化,因此它们往往会变慢一些。 JVM必须在运行时弄清楚,这是需要调用的确切方法。请注意,无论如何这都不是一个大问题,只有在您的目标是创建一个高性能的应用程序时,才应该考虑它。

例如,Apache Spark 2(在JVM上运行)中最大的优化之一是减少虚拟函数的分派,以获得更好的性能。