代码之家  ›  专栏  ›  技术社区  ›  GG.

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

  •  12
  • GG.  · 技术社区  · 15 年前

    我刚读到我们不应该过度使用虚函数。人们觉得虚拟功能越少,错误就越少,维护就越少。

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

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


    我能想到的一个原因是,由于V表查找,虚函数可能比普通函数慢。

    10 回复  |  直到 6 年前
        1
  •  14
  •   Stephen    15 年前

    你已经发表了一些笼统的声明,我认为大多数务实的程序员都会因为误传或误解而不屑一顾。但是,反虚拟狂热者确实存在,他们的代码对性能和维护也同样有害。

    在Java中,缺省情况下所有的东西都是虚拟的。说你不应该过度使用虚函数是很强的。

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

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

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

    人们觉得虚拟功能越少,错误就越少,维护就越少。 我不知道虚拟函数会出现什么样的错误和缺点。

    设计糟糕的代码很难维护。时期。

    如果您是一个库维护人员,调试隐藏在高类层次结构中的代码,那么很难跟踪代码实际执行的位置,如果没有强大的IDE的好处,通常很难判断哪个类重写了行为。它会导致在跟踪继承树的文件之间来回切换。

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

    • 保持你的等级制度肤浅。高大的树会使课堂混乱不堪。
    • 在C++中,如果您的类具有虚拟函数,则使用虚拟析构函数(如果不是,它可能是一个bug)。
    • 与任何层次结构一样,在派生类和基类之间保持“is-a”关系。
    • 您必须知道,可能根本不会调用虚拟函数…所以不要增加隐含的期望。
    • 有一个很难说的理由是虚拟函数速度较慢。它是动态绑定的,所以通常是这样。在引用的大多数案例中,它是否重要当然是有争议的。改为进行配置和优化:)
    • 在C++中,当不需要时不要使用虚拟。将一个函数标记为虚函数涉及到语义意义——不要滥用它。让读者知道“是的,这可能会被覆盖!”.
    • 与混合实现的层次结构相比,更喜欢纯虚拟接口。它更干净,更容易理解。

    实际情况是,虚拟函数非常有用,这些疑点不太可能来自平衡源——虚拟函数已经被广泛使用了很长时间。比其他语言更新的语言采用它们作为默认语言。

        2
  •  7
  •   Matt Brunell    15 年前

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

    我认为避免使用虚拟函数的最好原因是为了防止接口被滥用。

    写类以供扩展是一个好主意,但是有一件事 太开 . 通过仔细规划哪些函数是虚拟的,您可以控制(并保护)类的扩展方式。

    当一个类被扩展,从而破坏了基类的契约时,就会出现错误和维护问题。下面是一个例子:

    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();
        }
    }
    

    现在小部件不会碰到 NullReferenceException 后来。

        3
  •  6
  •   Igor Krivokon    15 年前

    每个依赖项都会增加代码的复杂性,并使维护变得更加困难。当您将函数定义为虚函数时,您将创建类对其他一些代码的依赖关系,这些代码目前可能还不存在。

    例如,在C中,您可以很容易地找到foo()的作用——只有一个foo()。在没有虚拟函数的C++中,它稍微复杂一些:你需要探索类和它的基类来找到我们需要的哪一个()。但至少可以提前确定地完成,而不是在运行时。对于虚拟函数,我们无法判断执行的是哪个foo(),因为它可以在一个子类中定义。

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

        4
  •  3
  •   Uri    15 年前

    我怀疑你误解了这句话。

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

    根据我的经验,有些学生在学习虚拟函数时,第一次忘记将函数变为虚拟函数而被烧掉, 认为只需将每个函数虚化是明智的 .

    由于虚拟函数在每个方法调用上都要付出代价(在C++中,由于单独编译通常无法避免),所以现在基本上为每一个方法调用付费,同时也防止内联。许多教师劝阻学生不要这样做,尽管“过度”一词是一个非常糟糕的选择。

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

        5
  •  2
  •   Sliq    15 年前

    在C++中:

    1. 虚拟功能有轻微的性能损失。通常情况下,它太小,无法产生任何差异,但在一个紧密的循环中,它可能是重要的。

    2. 虚函数将每个对象的大小增加一个指针。同样,这通常是无关紧要的,但如果你创造了数百万个小物体,这可能是一个因素。

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

    现在让我澄清一下:我是 说“不要使用虚拟功能”。它们是C++的重要和重要组成部分。只需意识到复杂性的潜在性。

        6
  •  2
  •   sharptooth    15 年前

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

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

    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++视图中停止为重载-它们变成了 新的、其他的、不相关的功能 .

    基类的默认实现没有标记为纯虚拟,因此派生类没有被强制重载默认实现。最后,只有在错误处理的情况下才调用该函数,而错误处理不是到处都使用。因此,这个臭虫被悄悄地引进,并在相当长的一段时间里不被注意。

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

        7
  •  1
  •   atamanroman    15 年前

    我不知道你在哪里读到的,但我不知道这和表演有关。

    也许它更多的是“更喜欢关于继承的组成”和如果你的类/方法不是最终的(可能主要是Java在这里)而可能发生的问题,但实际上并不是为了重用而设计的。有很多事情可能真的出错:

    • 也许你用虚拟方法 构造函数-一旦它们被重写, 您的基类调用重写的 方法,可以使用资源 在子类中初始化 构造器-稍后运行(NPE上升)。

    • 设想一个加法和一个addall方法 在列表类中。添加所有呼叫添加 很多时候都是虚拟的。 有人可以超越他们数数 在添加了多少项 所有。如果你不记录下这个数字 调用添加,开发者可以(和 will)重写add和addall (并添加一些counter++的东西到 他们)但是现在,如果你是阿达尔, 每一项计数两次(加和 addall)导致错误 结果和很难发现的错误。

    综上所述,如果您不为扩展而设计类(提供钩子,记录一些重要的实现内容),那么您根本不应该允许继承,因为这可能导致错误。如果需要的话,也可以很容易地从某个类中删除最后一个修饰符(或者为可重用性重新设计它),但不可能使非最终类(在子类化导致错误的情况下)成为最终类,因为其他类可能已经对它进行了子类化。

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

    在BooSCS有效Java中,关于这类信息的更多信息(这篇文章是在我阅读了第16项(“喜欢作文比继承”)和17(“设计和文档继承或其他禁止”)之后的几天写的。

        8
  •  0
  •   user207421    15 年前

    我在一个大约7年的时间里,在同一个C++系统上偶尔做顾问,检查大约4-5个程序员的工作。每次我回去,系统都变得越来越糟。在某种程度上,有人决定删除所有的虚拟功能,并用一个非常迟钝的基于工厂/RTTI的系统取代它们,这个系统基本上完成了虚拟功能已经完成的所有工作,但更糟的是,更昂贵的是,成千上万行代码,大量工作,大量测试,…完全和完全毫无意义,明显害怕未知的驱动力。

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

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

        9
  •  0
  •   Aquarius_Girl    13 年前

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

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

        10
  •  0
  •   Hamid Mohayeji    6 年前

    在爪哇,没有 virtual 关键字,但所有方法(函数)都是虚拟的,但标记为final、static方法和private实例方法的方法除外。使用虚拟函数一点也不坏,但是因为通常在编译时无法解决它们,并且编译器无法对它们执行优化,所以它们的速度会慢一些。JVM必须在运行时计算出,这是需要调用的确切方法。请注意,这无论如何都不是一个大问题,只有当您的目标是创建一个非常高性能的应用程序时,才应该考虑这个问题。

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