【问题标题】:When should you not use virtual destructors?什么时候不应该使用虚拟析构函数?
【发布时间】:2010-09-22 23:50:36
【问题描述】:

是否有充分的理由为类声明虚拟析构函数?你什么时候应该特别避免写一个?

【问题讨论】:

    标签: c++ virtual-functions virtual-destructor


    【解决方案1】:

    当满足以下任一条件时,无需使用虚拟析构函数:

    • 无意从中派生类
    • 堆上没有实例化
    • 无意通过指向超类的指针访问存储

    没有特别的理由避免它,除非你真的很想记忆。

    【讨论】:

    • 这不是一个好的答案。 “没有必要”不同于“不应该”,“无意”不同于“不可能”。
    • 另外补充:无意通过基类指针删除实例。
    • 这并不能真正回答问题。您不使用虚拟 dtor 的充分理由在哪里?
    • 我认为,当不需要做某事时,这是一个不做的好理由。它遵循 XP 的简单设计原则。
    • 通过说您“无意”,您对您的课程将如何被使用做出了巨大的假设。在我看来,在大多数情况下(因此应该是默认的)最简单的解决方案应该是拥有虚拟析构函数,并且只有在您有特定理由不这样做时才避免使用它们。所以我仍然很好奇什么是一个很好的理由。
    【解决方案2】:

    当且仅当我有虚拟方法时,我才声明一个虚拟析构函数。一旦有了虚方法,我就不相信自己会避免在堆上实例化它或存储指向基类的指针。这两个都是非常常见的操作,如果析构函数没有声明为虚拟的,通常会悄悄地泄漏资源。

    【讨论】:

    • 事实上,gcc 上有一个警告选项,它会针对这种情况发出警告(虚拟方法,但没有虚拟 dtor)。
    • 如果你从类派生,不管你是否有其他虚函数,你不会冒内存泄漏的风险吗?
    • 我同意 mag。虚拟析构函数和/或虚拟方法的这种使用是单独的要求。虚拟析构函数为类提供执行清理的能力(例如删除内存、关闭文件等),并确保调用其所有成员的构造函数。
    • @MagRoader 理论上你会这样做,但是因为如果你存储(和删除)指向在堆上分配的派生对象的指针在指向基础的指针中,你只会遇到问题,显而易见的问题是那里有什么用是那个没有虚函数的指针吗?我只能看到一种可能性:您只能在“完成”时使用对象删除资源;在这种情况下,你应该有一个没有任何其他方法的虚拟析构函数。
    【解决方案3】:

    只要有可能在指向具有您的类类型的子类对象的指针上调用delete 时,就需要一个虚拟析构函数。这确保在运行时调用正确的析构函数,而编译器不必在编译时知道堆上对象的类。例如,假设BA 的子类:

    A *x = new B;
    delete x;     // ~B() called, even though x has type A*
    

    如果您的代码不是性能关键,那么为了安全起见,为您编写的每个基类添加一个虚拟析构函数是合理的。

    但是,如果您发现自己delete在一个紧密的循环中处理了很多对象,那么调用虚函数(即使是空的)的性能开销可能会很明显。编译器通常不能内联这些调用,处理器可能很难预测去哪里。这不太可能对性能产生重大影响,但值得一提。

    【讨论】:

    • “如果你的代码不是性能关键,那么为了安全起见,为你编写的每个基类添加一个虚拟析构函数是合理的。”在我看到的每个答案中都应该更加强调
    【解决方案4】:

    性能答案是我所知道的唯一一个有可能为真的答案。如果您已经测量并发现对析构函数进行去虚拟化确实加快了速度,那么您可能在该类中还有其他需要加速的东西,但此时还有更重要的考虑因素。总有一天,有人会发现您的代码会为他们提供一个很好的基类,并为他们节省一周的工作量。你最好确保他们完成那一周的工作,复制和粘贴你的代码,而不是使用你的代码作为基础。你最好确保你的一些重要方法是私有的,这样任何人都不能继承你。

    【讨论】:

    • 多态性肯定会减慢速度。将其与我们需要多态并选择不使用的情况进行比较,它会更慢。示例:我们在基类析构函数中实现所有逻辑,使用 RTTI 和 switch 语句来清理资源。
    • 在 C++ 中,阻止我从您记录的不适合用作基类的类继承不是您的责任。谨慎使用继承是我的责任。当然,除非房屋风格指南另有说明。
    • ... 只是将析构函数设为虚拟并不意味着该类必须作为基类正常工作。因此,将其标记为“仅仅因为”而不是进行评估,而是写一张我的代码无法兑现的支票。
    【解决方案5】:

    我通常将析构函数声明为虚拟,但如果您有在内部循环中使用的性能关键代码,您可能希望避免虚拟表查找。在某些情况下,这可能很重要,例如碰撞检查。但是,如果使用继承,请注意如何销毁这些对象,否则只会销毁对象的一半。

    请注意,如果该对象上的 any 方法是虚拟的,则会针对该对象进行虚拟表查找。因此,如果您在类中有其他虚拟方法,则删除析构函数上的虚拟规范毫无意义。

    【讨论】:

      【解决方案6】:

      明确回答这个问题,即你什么时候应该声明一个虚拟析构函数。

      C++ '98/'03

      添加虚拟析构函数可能会将您的类从 POD (plain old data)* 或聚合更改为非 POD。如果您的类类型在某处进行了聚合初始化,这可能会阻止您的项目编译。

      struct A {
        // virtual ~A ();
        int i;
        int j;
      };
      void foo () { 
        A a = { 0, 1 };  // Will fail if virtual dtor declared
      }
      

      在极端情况下,此类更改还可能导致未定义的行为,其中类以需要 POD 的方式使用,例如通过省略号参数传递它,或与 memcpy 一起使用。

      void bar (...);
      void foo (A & a) { 
        bar (a);  // Undefined behavior if virtual dtor declared
      }
      

      [* POD 类型是对其内存布局有特定保证的类型。该标准实际上只是说,如果您要从具有 POD 类型的对象复制到一个字符数组(或无符号字符)中并再次返回,那么结果将与原始对象相同。]

      现代 C++

      在最近的 C++ 版本中,POD 的概念分为类布局及其构造、复制和销毁。

      对于省略号的情况,它不再是未定义的行为,它现在由实现定义的语义有条件地支持(N3937 - ~C++ '14 - 5.2.2/7):

      ...传递具有非平凡复制构造函数、非平凡移动构造函数或非平凡析构函数且没有相应参数的类类型(第 9 条)的潜在评估参数是有条件支持的具有实现定义的语义。

      声明 =default 以外的析构函数将意味着它不是微不足道的 (12.4/5)

      ...如果不是用户提供的析构函数,那么它是微不足道的...

      对现代 C++ 的其他更改减少了聚合初始化问题的影响,因为可以添加构造函数:

      struct A {
        A(int i, int j);
        virtual ~A ();
        int i;
      
        int j;
      };
      void foo () { 
        A a = { 0, 1 };  // OK
      }
      

      【讨论】:

      • 你是对的,我错了,性能不是唯一的原因。但这表明我对其余部分的看法是正确的:类的程序员最好包含代码以防止该类被其他任何人继承。
      • 亲爱的理查德,您能否对您所写的内容发表更多评论。我不明白你的观点,但这似乎是我通过谷歌搜索发现的唯一有价值的观点)或者你可以提供更详细解释的链接?
      • @JohnSmith 我已经更新了答案。希望这会有所帮助。
      【解决方案7】:

      虚拟函数意味着每个分配的对象都会增加一个虚拟函数表指针的内存成本。

      因此,如果您的程序涉及分配大量对象,则值得避免使用所有虚函数,以便为每个对象节省额外的 32 位。

      在所有其他情况下,您将省去调试痛苦以使 dtor 虚拟化。

      【讨论】:

      • 只是吹毛求疵,但现在指针通常是 64 位而不是 32 位。
      【解决方案8】:

      并非所有 C++ 类都适合用作具有动态多态性的基类。

      如果你想让你的类适合动态多态,那么它的析构函数必须是虚拟的。此外,子类可能想要覆盖的任何方法(这可能意味着所有公共方法,以及可能在内部使用的一些受保护的方法)必须是虚拟的。

      如果你的类不适合动态多态,那么析构函数不应该被标记为虚拟的,因为这样做会产生误导。它只会鼓励人们错误地使用您的课程。

      这是一个不适合动态多态的类的示例,即使它的析构函数是虚拟的:

      class MutexLock {
          mutex *mtx_;
      public:
          explicit MutexLock(mutex *mtx) : mtx_(mtx) { mtx_->lock(); }
          ~MutexLock() { mtx_->unlock(); }
      private:
          MutexLock(const MutexLock &rhs);
          MutexLock &operator=(const MutexLock &rhs);
      };
      

      这个类的重点是坐在堆栈上为 RAII。如果您传递指向此类对象的指针,更不用说它的子类了,那么您做错了。

      【讨论】:

      • 多态使用并不意味着多态删除。一个类有很多用例有虚方法但没有虚析构函数。考虑一个典型的静态定义对话框,几乎在任何 GUI 工具包中。父窗口将销毁子对象,它知道每个子对象的确切类型,但所有子窗口也将在任意数量的地方多态地使用,例如命中测试、绘图、获取文本的可访问性 API-语音引擎等。
      • 是的,但提问者问你什么时候应该特别避免使用虚拟析构函数。对于您描述的对话框,虚拟析构函数毫无意义,但 IMO 无害。我不确定我是否有信心永远不需要使用基类指针删除对话框——例如,我将来可能希望我的父窗口使用工厂创建它的子对象。所以这不是避免虚拟析构函数的问题,只是你可能不会费心拥有一个。但是,不适合派生的类上的虚拟析构函数有害的,因为它具有误导性。
      【解决方案9】:

      如果您有一个包含大量实例的非常小的类,则 vtable 指针的开销可能会影响程序的内存使用量。只要您的类没有任何其他虚拟方法,将析构函数设为非虚拟就可以节省开销。

      【讨论】:

        【解决方案10】:

        在基类上执行的操作应该是虚拟的,应该是虚拟的。如果可以通过基类接口多态地执行删除,那么它的行为必须是虚拟的并且是虚拟的。

        如果您不打算从类派生,则析构函数不需要是虚拟的。即使你这样做了,如果不需要删除基类指针,一个受保护的非虚拟析构函数也一样好

        【讨论】:

          【解决方案11】:

          如果您绝对肯定必须确保您的类没有 vtable,那么您也不能有虚拟析构函数。

          这种情况很少见,但确实会发生。

          最熟悉的模式示例是 DirectX D3DVECTOR 和 D3DMATRIX 类。这些是类方法而不是语法糖的函数,但是这些类故意没有 vtable 以避免函数开销,因为这些类专门用于许多高性能应用程序的内部循环中。

          【讨论】:

            【解决方案12】:

            不将析构函数声明为虚拟的一个很好的理由是,这样可以使您的类免于添加虚拟函数表,并且您应该尽可能避免这种情况。

            我知道很多人喜欢总是将析构函数声明为虚拟的,只是为了安全起见。但是,如果您的类没有任何其他虚函数,那么拥有虚析构函数真的非常没有意义。即使您将您的课程提供给其他人,然后他们从中派生出其他课程,那么他们也没有理由在向上转换到您的课程的指针上调用 delete - 如果他们这样做,那么我会认为这是一个错误。

            好的,有一个例外,即如果您的类(错误)用于执行派生对象的多态删除,但您 - 或其他人 - 希望知道这需要一个虚拟析构函数。

            换句话说,如果你的类有一个非虚拟析构函数,那么这是一个非常明确的声明:“不要用我来删除派生对象!”

            【讨论】:

              猜你喜欢
              • 2010-11-10
              • 2019-12-26
              • 2013-10-17
              • 2011-06-21
              • 2020-07-16
              • 2018-10-23
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多