最终的虚拟功能有什么意义?

What's the point of a final virtual function?

本文关键字:什么 功能 虚拟      更新时间:2023-10-16

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

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

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

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

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


由于它在评论中被提到了两次,我想补充一点:

一些人给出的基类将非重写方法声明为final的一个原因是,任何试图在派生类中定义该方法的人都会出错,而不是默默地创建一个"隐藏"基类方法的方法。

struct Base {
   void test() { std::cout << "Base::test()n"; }
};
void run(Base *o) {
    o->test();
}

// Some other developer derives a class
struct Derived : Base {
   void test() { std::cout << "Derived::test()n"; }
};
int main() {
    Derived o;
    o.test();
    run(&o);
}

Base的开发人员不希望Derived的开发人员这样做,并希望它产生一个错误。所以他们写道:

struct Base {
    virtual void test() final { ... }
};

使用Base::foo()的这个声明会导致Derived的定义产生一个错误,比如:

<source>:14:13: error: declaration of 'test' overrides a 'final' function
       void test() { std::cout << "Derived::test()n"; }
            ^
<source>:4:22: note: overridden virtual function is here
        virtual void test() final { std::cout << "Base::test()n"; }
                     ^

您可以自己决定这个目的是否值得,但我想指出的是,声明函数virtual final并不是防止这种隐藏的完整解决方案。派生类仍然可以隐藏Base::test(),而不会引发所需的编译器错误:

struct Derived : Base {
   void test(int = 0) { std::cout << "Derived::test()n"; }
};

无论Base::test()是否为virtual finalDerived的定义都是有效的,并且代码Derived o; o.test(); run(&o);的行为完全相同。

至于对用户的明确声明,我个人认为,不标记方法virtual比标记virtual final更清楚地向用户声明该方法不打算被重写。但我想哪种方式更清晰取决于开发人员阅读代码以及他们熟悉的约定。

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

[…]为了方便起见,我们说任何虚拟函数都会覆盖它自己。

第4段:

如果某个类B中的虚拟函数f用virt说明符final标记,并且在派生自函数D::f覆盖B::f,则程序格式错误。[…]

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

编辑:要完全清楚:"要点"是关于为什么使用虚拟的问题。使用它的根本原因是(i)因为代码不会以其他方式编译,以及(ii)为什么在一个类足够的情况下使用更多的类使示例更加复杂?因此,仅使用一个具有虚拟最终函数的类作为示例。

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

一种可能的用途是,如果你不想f真的被高估,但你仍然想生成一个vtable,但这仍然是一种可怕的做法。

除了上面的好答案之外,还有一个著名的final应用程序(非常受Java启发)。假设我们在基类中定义了一个函数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很容易实现最终效果。

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

这个例子的目的是说明final是如何工作的,它就是这样做的。

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

struct Base2 {
    virtual void f() final;
};
struct Base1 {
};
assert(sizeof(Base2) != sizeof(Base1)); //probably

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

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

// 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() {}

以下是您可能实际选择在基类中声明函数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可以防止在派生类中声明具有相同名称和签名的函数。

而不是:

public:
    virtual void f();

我发现写这个很有用:

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

主要原因是,在调度到任何可能被覆盖的实现之前,您现在都有一个断点。遗憾的是(IMHO),说"final"也需要说"virtual"。

我发现了另一种情况,在这种情况下,将for virtual函数声明为final是有用的。这个案例是SonarQube警告列表的一部分。警告说明中写道:

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

例如:
-根据约定,子类构造函数从调用父类构造函数开始
-父类构造函数调用的是父成员函数,而不是子类中重写的函数,这让子类的开发人员感到困惑
-如果成员函数在父类中是纯虚拟的,它可能会产生未定义的行为。

不符合代码示例

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"
};

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

关于virtualfinal语法,维基百科的例子通过引入struct Base2 : Base1,其中Base1包含virtual void f();,Base2包含void f() final;(见下文)将更具表达性。

标准

参考N3690:

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

不存在必须同时使用关键字virtual和具有特殊含义的标识符final的规则。第8.4节,功能定义(注意opt=可选):

功能定义:

属性说明符seq(opt)decl说明符seq

实习

在C++11中,使用final时可以省略virtual关键字。这是在gcc>4.7.1、clang>3.0和C++11、msvc上编译的。。。(请参阅编译器资源管理器)。

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

PS:cppreference上的示例也没有在同一声明中同时使用virtual和final。

PPS:override也是如此。

我认为大多数答案都忽略了一个重要的点。final意味着在指定之后不再有override。在基类上标记它几乎毫无意义。

当派生类可能被进一步派生时,它可以使用final将给定方法的实现锁定到它提供的方法。

#include <iostream>
class A {
    public:
    virtual void foo() = 0;
    virtual void bar() = 0;
};
class B : public A {
    public:
    void foo() final override { std::cout<<"B::foo()"<<std::endl; }
    void bar() override { std::cout<<"B::bar()"<<std::endl; }
};
class C : public B {
    public:
    // can't do this as B marked ::foo final!
    // void foo() override { std::cout<<"C::foo()"<<std::endl; }
    void bar() override { std::cout<<"C::bar()"<<std::endl; }
};