【问题标题】:What's the point of a final virtual function?最终的虚函数有什么意义?
【发布时间】:2012-07-27 02:41:18
【问题描述】:

Wikipedia 在 C++11 final 修饰符上有以下示例:

struct Base2 {
    virtual void f() final;
};

struct Derived2 : Base2 {
    void f(); // ill-formed because the virtual function Base2::f has been marked final
};

我不明白引入虚函数并立即将其标记为 final 的意义。这只是一个不好的例子,还是还有更多?

【问题讨论】:

  • 嗯,Java 有它,所以你知道,C++ 也必须有它。
  • 我猜这只是个坏例子
  • 有趣的事实:(几乎)相同的例子可以在 §10.3/4 下的标准中找到。
  • 你总是可以通过使用关键字作为标识符来迷惑人们:int final = 7; 如果你想要 Stroustrup 的演讲,see here
  • “这是一个例子”的哪一部分难以理解?大多数其他示例代码也同样毫无意义。示例的目的是展示功能的工作原理。

标签: c++ c++11 inheritance final virtual-functions


【解决方案1】:

通常final 不会用于基类的虚函数定义。 final 将由重写函数的派生类使用,以防止进一步的派生类型进一步重写函数。由于覆盖函数通常必须是虚拟的,这意味着任何人都可以在进一步的派生类型中覆盖该函数。 final 允许指定一个覆盖另一个但自身不能覆盖的函数。

例如,如果您正在设计一个类层次结构并需要覆盖一个函数,但您不希望该类层次结构的用户也这样做,那么您可以在派生类中将这些函数标记为 final。

【讨论】:

  • 如果final 隐含的意思是override final 岂不是有意义?
  • 我看不出有什么理由不这样做,但我也看不出有什么强烈的理由这样做,因为您已经可以标记函数final override。也许应该有一个样式警告,要求所有final 函数也像其他virtualoverride 样式警告一样标记为override
  • 接口中的最终方法将是“基”类中最终方法的一个示例。如果有人在子类中定义了具有相同名称的函数,'final' 也会产生错误或至少是警告。问题是编译器是否会立即去虚拟化这种情况。
  • @Trass3r 不可能实现final 接口方法,因为在 C++ 中实现接口方法需要重写。这样一个无法实现的接口是没有用的。
  • 是的,这就是你在接口中实现它的原因。
【解决方案2】:

对于要标记为 final 的函数,它必须是 virtual,即在 C++11 §10.3 段中。 2:

[...] 为方便起见,我们说任何虚函数都会覆盖自身。

和第 4 段:

如果某个类 B 中的虚函数 f 用 virt-specifier final 标记,并且在派生自的类 D 中 B 一个函数 D::f 覆盖 B::f,程序格式错误。 [...]

即,final 只需要与虚函数(或与阻止继承的类)一起使用。因此,该示例需要使用 virtual 才能使其成为有效的 C++ 代码。

编辑:完全清楚:“点”询问了为什么甚至使用虚拟的问题。使用它的底线原因是(i)因为代码不会编译,以及,(ii)为什么在一个足够的情况下使用更多类使示例更复杂?因此,仅以一个具有 virtual final 函数的类为例。

【讨论】:

  • 尊敬的,我完全明白问题的重点。 “点”询问了为什么甚至使用虚拟的问题。使用它的最根本原因是因为代码不会编译,为什么在一个足够的情况下使用更多类使示例更复杂?因此,恰好一个具有虚拟最终函数的类被用作示例。 QED。
  • @Luchian Grigore:我添加了一个完全清楚的编辑,因为我没有用“底线原因”来结束我的答案。也许为什么这个问题会引起这么多 cmets 是因为每个人都会自然地看着这个例子并说,“为什么有人会使用那个代码?”而不是从示例作者的角度来看它。大多数例子都是为了实用而编写的——这个例子只是为了做一个最小的例子来展示它的行为。
【解决方案3】:

这对我来说似乎一点用都没有。我认为这只是一个演示语法的示例。

一种可能的用途是,如果您不希望 f 真正被覆盖,但您仍想生成一个 vtable,但这仍然是一种可怕的做事方式。

【讨论】:

  • 最终和虚拟是两个不同的方面。它在覆盖与重载上下文中变得相关。虚拟限定符意味着运行时类型推断。非虚拟意味着编译类型类型推断。当涉及重载并且涉及类型提升/转换时,非虚拟类型可能会导致有趣的结果。通常,尽管我们鼓励您不要编写那种代码。
  • 如果编译器不生成 vtable,为什么还要生成?
  • @sasha.sochka 例如,我认为dynamic_cast 没有一个就行不通。但确保 vtable 的常用方法是将析构函数设为虚拟。
【解决方案4】:

我不明白引入虚函数并立即将其标记为 final 的意义。

该示例的目的是说明final 的工作原理,它就是这样做的。

一个实用的目的可能是看看 vtable 如何影响类的大小。

struct Base2 {
    virtual void f() final;
};
struct Base1 {
};

assert(sizeof(Base2) != sizeof(Base1)); //probably

Base2 可以简单地用于测试平台细节,覆盖f() 是没有意义的,因为它只是用于测试目的,所以它被标记为final。当然,如果您这样做,那么设计中就有问题。我个人不会为了检查vfptr 的大小而创建一个带有virtual 函数的类。

【讨论】:

    【解决方案5】:

    在重构遗留代码时(例如,从母类中删除虚拟方法),这有助于确保没有子类使用此虚拟函数。

    // Removing foo method is not impacting any child class => this compiles
    struct NoImpact { virtual void foo() final {} };
    struct OK : NoImpact {};
    
    // Removing foo method is impacting a child class => NOK class does not compile
    struct ImpactChildClass { virtual void foo() final {} };
    struct NOK : ImpactChildClass { void foo() {} };
    
    int main() {}
    

    【讨论】:

      【解决方案6】:

      除了上面的好答案 - 这是一个众所周知的 final 应用程序(非常受 Java 启发)。假设我们在 Base 类中定义了一个函数 wait(),并且我们希望在它的所有后代中只有一个 wait() 实现。在这种情况下,我们可以将 wait() 声明为 final。

      例如:

      class Base { 
         public: 
             virtual void wait() final { cout << "I m inside Base::wait()" << endl; }
             void wait_non_final() { cout << "I m inside Base::wait_non_final()" << endl; }
      }; 
      

      这里是派生类的定义:

      class Derived : public Base {
            public: 
              // assume programmer had no idea there is a function Base::wait() 
      
              // error: wait is final
              void wait() { cout << "I am inside Derived::wait() \n"; } 
              // that's ok    
              void wait_non_final() { cout << "I am inside Derived::wait_non_final(); }
      
      } 
      

      如果 wait() 是一个纯虚函数,那将是无用(并且不正确)。在这种情况下:编译器会要求您在派生类中定义 wait()。如果你这样做,它会给你一个错误,因为 wait() 是最终的。

      为什么最终函数应该是虚函数? (这也令人困惑)因为(imo)1)final的概念非常接近虚函数的概念【虚函数有很多实现——final函数只有一种实现】,2)很容易使用 vtables 实现最终效果。

      【讨论】:

      • 这个答案实际上解释了一切,而不是简单地说“否则它会是错误的”或“因为它是一个例子”,恕我直言,这应该被接受为这个问题的答案。
      【解决方案7】:

      这就是为什么您实际上可能会选择在基类中同时声明 virtualfinal 的函数:

      class A {
          void f();
      };
      
      class B : public A {
          void f(); // Compiles fine!
      };
      
      class C {
          virtual void f() final;
      };
      
      class D : public C {
          void f(); // Generates error.
      };
      

      标记为final的函数必须也是virtual。标记函数final 可防止您在派生类中声明具有相同名称和签名的函数。

      【讨论】:

        【解决方案8】:

        而不是这个:

        public:
            virtual void f();
        

        我觉得这样写很有用:

        public:
            virtual void f() final
                {
                do_f(); // breakpoint here
                }
        protected:
            virtual void do_f();
        

        主要原因是您现在有一个断点位置,然后再分派到任何可能被覆盖的实现中的任何一个。可悲的是(恕我直言),说“最终”也需要说“虚拟”。

        【讨论】:

          【解决方案9】:

          我发现了另一种情况,将虚函数声明为 final 很有用。这个案例是SonarQube list of warnings 的一部分。警告描述说:

          在实例化覆盖成员函数的子类时,从构造函数或析构函数调用可覆盖的成员函数可能会导致意外行为。

          例如:
          - 按照约定,子类构造函数首先调用父类构造函数。
          - 父类构造函数调用的是父成员函数,而不是子类中重写的那个,这对子类的开发者来说很困惑。
          - 如果成员函数在父类中是纯虚函数,它会产生未定义的行为。

          不合规代码示例

          class Parent {
            public:
              Parent() {
                method1();
                method2(); // Noncompliant; confusing because Parent::method2() will always been called even if the method is overridden
              }
              virtual ~Parent() {
                method3(); // Noncompliant; undefined behavior (ex: throws a "pure virtual method called" exception)
              }
            protected:
              void         method1() { /*...*/ }
              virtual void method2() { /*...*/ }
              virtual void method3() = 0; // pure virtual
          };
          
          class Child : public Parent {
            public:
              Child() { // leads to a call to Parent::method2(), not Child::method2()
              }
              virtual ~Child() {
                method3(); // Noncompliant; Child::method3() will always be called even if a child class overrides method3
              }
            protected:
              void method2() override { /*...*/ }
              void method3() override { /*...*/ }
          };
          

          合规解决方案

          class Parent {
            public:
              Parent() {
                method1();
                Parent::method2(); // acceptable but poor design
              }
              virtual ~Parent() {
                // call to pure virtual function removed
              }
            protected:
              void         method1() { /*...*/ }
              virtual void method2() { /*...*/ }
              virtual void method3() = 0;
          };
          
          class Child : public Parent {
            public:
              Child() {
              }
              virtual ~Child() {
                method3(); // method3() is now final so this is okay
              }
            protected:
              void method2() override { /*...*/ }
              void method3() final    { /*...*/ } // this virtual function is "final"
          };
          

          【讨论】:

            【解决方案10】:

            virtual + final 用于一个函数声明中,以缩短示例。

            关于virtualfinal语法,通过引入struct Base2 : Base1 与Base1 包含virtual void f(); 和Base2 包含void f() final; (见下文)将更具表现力.

            标准

            参考N3690

            • virtual 作为function-specifier 可以是decl-specifier-seq 的一部分
            • final 可以是 virt-specifier-seq 的一部分

            没有规则必须将关键字 virtual具有特殊含义的标识符 final 一起使用。第 8.4 节,函数定义(注意 opt = 可选):

            函数定义:

            attribute-specifier-seq(opt) decl-specifier-seq(opt) 声明符 virt-specifier-seq(opt) function-body

            练习

            在 C++11 中,您可以在使用 final 时省略 virtual 关键字。这可以在 gcc >4.7.1、clang >3.0 和 C++11、msvc 上编译,...(请参阅 compiler explorer)。

            struct A
            {
                virtual void f() {}
            };
            
            struct B : A
            {
                void f() final {}
            };
            
            int main()
            {
                auto b = B();
                b.f();
            }
            

            PS:example on cppreference 也不会在同一声明中将 virtual 与 final 一起使用。

            PPS:同样适用于override

            【讨论】:

              猜你喜欢
              • 2011-04-27
              • 2013-05-20
              • 2012-10-06
              • 2014-02-12
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2018-03-08
              • 2016-09-03
              相关资源
              最近更新 更多