一个最终虚函数的意义是什么?

74

维基百科提供了以下关于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的意义所在。这是一个糟糕的例子,还是有更多的含义?


3
好的,Java 有这个功能,所以你知道,C++ 也必须有它。 - chris
13
我想那只是一个糟糕的例子。 - Man of One Way
3
有趣的事实:(几乎)相同的示例可以在标准的第§10.3/4节中找到。 - Xeo
3
你可以使用关键字作为标识符来迷惑人们,例如: int final = 7; 如果你想了解 Stroustrup 对此的讲解,请点击这里 - chris
3
“这是一个示例”的哪部分难以理解?大多数其他示例代码同样毫无意义。示例的目的是展示功能如何工作。 - Nicol Bolas
显示剩余5条评论
11个回答

79

通常不会在基类虚函数的定义中使用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 final`还是非`virtual final`,这个`Derived`的定义都是有效的,并且代码`Derived o; o.test(); run(&o);`的行为完全相同。
至于对于用户的清晰说明,个人认为仅仅不将一个方法标记为`virtual`比标记为`virtual final`更清晰地告诉用户该方法不打算被覆盖,但我想哪种方式更清晰取决于阅读代码的开发者和他们熟悉的约定。

2
那么,如果“final”隐含地意味着“override final”,这不是很有道理吗? - fredoverflow
5
我不认为有任何理由不这样做,但我也没有看到有很强的理由去这样做,因为你已经可以将函数标记为final override。也许应该有一个风格警告,要求所有的final函数像其他virtualoverride风格警告一样被标记为override - bames53
2
@Trass3r 无法实现final接口方法,因为在C++中实现接口方法需要覆盖。这样一个无法实现的接口是没有用处的。 - bames53
是的,这就是为什么你要在接口中实现它的原因。 - Trass3r
@Trass3r 啊,我明白你在说什么。然而,我不认为这种方法很好;它不能防止隐藏,因为你仍然可以在派生类中声明同名但参数不同的函数(这些函数可以使用默认参数以实际上具有相同的签名,而且通常会有关于隐藏的警告)。另外,你只能重写虚函数,所以如果不将基类函数标记为虚函数就可以阻止重写。最后,这样使用virtual对我来说看起来像是混淆。 - bames53
显示剩余5条评论

12
为了使函数被标记为 "final",它必须是 "virtual" 的,在 C++11 §10.3 段的第2段中说明:"[...] 为了方便起见,我们说任何虚函数都会覆盖自己。" 在第4段中说明:如果某个类 B 中的虚函数 f 标记为 final,并且在派生自 B 的类 D 中,函数 D::f 覆盖了 B::f,则程序是非法的。也就是说,只能将 "final" 用于虚函数(或用于阻止继承的类)。因此,该示例要想成为有效的 C++ 代码,需要使用 "virtual"。
编辑:为了更加清楚,关于所问的 "要点" 是为什么要使用 virtual。使用 virtual 的根本原因是 (i) 否则代码无法编译,以及 (ii) 使用更多类使示例变得更加复杂有何意义?因此,只使用一个具有虚 final 函数的类作为示例。

恭敬地说,我完全理解问题的要点。所问的“要点”关注的是为什么使用虚函数。使用它的根本原因是因为否则代码将无法编译,而且为什么要使用更多的类使示例更加复杂,当一个类就足够了呢?因此,只使用一个带有虚终函数的类作为示例。证毕。 - Paul Preney
@Luchian Grigore:我添加了一个编辑,以便完全清楚,因为我没有用“底线原因”来总结我的答案。也许这个问题之所以会得到这么多评论,是因为每个人都自然而然地看着这个例子,说:“为什么有人要使用那段代码?”而不是从示例作者的角度来看待它。大多数示例都是为了实际使用而编写的——除了展示它的行为方式的最小示例外,这个示例并没有其他实际用途。 - Paul Preney

11

我觉得这个对我来说完全没用,我认为这只是一个示例,用来展示语法。

有一个可能的用途是,如果你不想让 f 被真正重载,但你仍然想生成一个虚函数表,但这仍然是一种可怕的做法。


Final和Virtual是两个不同的方面。它在覆盖与重载的上下文中变得相关。虚拟限定符意味着运行时类型推断。非虚拟意味着编译类型推断。当涉及重载和类型提升/转换时,非虚拟类型可能会导致有趣的结果。通常,您被鼓励不要编写那种代码。 - VSOverFlow
如果编译器不生成vtable,为什么你还想要生成它呢? - sasha.sochka
5
@sasha.sochka,例如我认为如果没有虚函数表,dynamic_cast 就无法工作。但通常确保有虚函数表的方法是将析构函数声明为虚函数。 - Mark Ransom

8

在上面的好回答中,这里有一个很出名的 final 应用(非常受 Java 启发)。假设我们在一个基类中定义了一个 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()是final的。

为什么一个final函数应该是虚函数?(这也很令人困惑) 因为(我认为)1)final的概念与虚函数的概念非常接近[虚函数有多种实现- final函数只有一种实现],2)使用虚表格很容易实现最终效果。


这个答案实际上解释了一切,而不仅仅是简单地陈述“否则就会错”或“因为这是一个例子”,在我看来,这应该被接受为这个问题的答案。 - Troyseph

6

我不理解为什么要引入一个虚函数然后立即将其标记为final。

那个例子的目的是为了说明final的作用,并且它确实做到了。

实际应用可能是看看虚表如何影响类的大小。

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

assert(sizeof(Base2) != sizeof(Base1)); //probably
< p > Base2 可以用于测试平台特定的内容,而且没有必要覆盖 f() ,因为它只是用于测试目的,所以标记为 final 。当然,如果你这样做,说明设计存在问题。个人而言,我不会创建一个带有 virtual 函数的类来检查 vfptr 的大小。


6

在重构旧代码时(例如从母类中删除虚方法),这很有用,以确保没有子类正在使用此虚函数。

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

2

以下是为什么您可能需要在基类中同时声明一个函数为 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可以防止在派生类中声明具有相同名称和签名的函数。


final就像override,一个final函数不需要标记为virtual,事实上,你应该只指定一个,根据ISOCPP的说法。 - bobobobo
@bobobobo - 即使在父类中从未标记为virtual,这样做也有效吗? - Omnifarious

1

而不是这样:

public:
    virtual void f();

我发现写下这句话很有用:

I find it useful to write this:

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

主要原因是现在你有一个单独的断点位置,在调度到潜在的多个重写实现之前。不幸的是(依我之见),说“final”也需要说“virtual”。

0

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节,函数定义(heed opt = optional):

函数定义:

属性说明符序列(可选) 声明说明符序列(可选) 声明符 虚拟说明符序列(可选) 函数体

练习

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

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

另外,对于override也是如此。


0

我发现另一个情况,虚函数声明为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"
};

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接