"final"是否意味着覆盖重写?

74
我理解中,override 关键字表示一个给定的声明实现了一个基类 virtual 方法,并且如果没有找到匹配的基方法,则应该编译失败。
我对 final 关键字的理解是它告诉编译器不允许任何类覆盖此 virtual 函数。
那么,override final 是多余的吗? 似乎可以编译通过 override final 传达了哪些信息,而 final 却没有呢?这种组合的使用情况是什么?
5个回答

68

final本身并不要求函数首先覆盖任何东西。它在[class.virtual] / 4中定义为:

  

如果某个类的虚拟函数f被标记为    virt-specifier final ,而在从派生的类中,函数重载了,则该程序是非法的。

就这样。现在override final 仅意味着
“此函数覆盖了一个基类函数( override ),并且自身不能被覆盖( final )。”
final 本身会施加较弱的要求。 override 和 final 具有独立的行为。


注意, final 只能用于虚拟函数 - [class.mem] / 8

  

virt-specifier-seq 只能出现在   虚拟成员函数的声明(10.3)。

因此声明如下:

void foo() final;

实际上相当于

virtual void foo() final override;

由于两者都需要使用foo来覆盖某些东西 - 第二个声明通过使用override,第一个声明是当且仅当foo隐式地成为虚拟函数时有效,也就是说foo在基类中重写一个名为foo的虚拟函数时,这使得派生类中的foo自动变成虚拟函数。 因此,在只有final但不是virtual的情况下,override是多余的。
不过,后面的声明更清楚地表达了意图,应该优先选择。


12
我喜欢你的回答,但我想澄清一下,从实际角度来看,virtual void f() final overridevoid f() final在某种意义上是等价的,因为两者如果没有覆盖任何东西都会失败。final仅适用于虚函数,而后一种声明的f只有当它覆盖一个函数时才是虚函数。尽管如此,后者的错误消息可能不太精确。 - Markus Mayr
7
我不这么认为。基于你提供的描述,我更喜欢第一种声明方式,原因与我避免在重写虚方法时添加 virtual 相同:它没有增加任何价值。但是这种写法当然比写成 virtual void foo() final; 好。然而,后面的声明表达意图更清晰。 - Wolf
1
@Wolf,你不认为这样表达更清晰吗?你能否明确表述第一种声明形式所表达的意图是什么? - Columbo
2
根据您的描述,我认为final必须是virtual(我正在学习这些新功能),并且由于您省略了virtual关键字,如果它不是override,则编译将无法通过。或者也许我忽略了一种可能导致不良行为的情况?在下面Angew的答案中,我只是试图弄清楚非覆盖终极虚拟的用法... - Wolf
3
很可能这就是为什么例如Google C++样式指南建议“使用一个 override 或 (较少使用) final 说明符对虚函数或虚析构函数的重载进行注释。声明重载时不要使用 virtual”。 - Sonic78
显示剩余6条评论

25

final并不一定意味着该函数被覆盖。如果在继承层次结构的第一个声明中将虚函数声明为final,这是完全有效的(尽管有些可疑)。

我能想到创建虚函数并立即使用final的一个原因是,如果您想要防止派生类为相同名称和参数赋予不同的含义。


1
展示虚拟/立即-final函数可行用例加1。但是我想知道,当这样做时编译器是否仍会生成vtable呢? - Carlton
2
@Carlton 这取决于编译器。就标准而言,它使类具有多态性,这意味着 dynamic_cast 必须为其工作,例如。我相信虚函数的 vtable 实现也需要 vtable 才能使 dynamic_cast 工作。 - Angew is no longer proud of SO
啊,我刚刚根据我的先前评论发布了一个问题,但你在这里基本上已经回答了它。谢谢。 - Carlton
3
如果您希望防止派生类给相同的名称和参数赋予不同的含义,就不要将其设为虚方法。我并没有像@Carlton那样在这种含糊的描述中看到一个可行的用例,但我真的很想知道一个。 - Wolf
@Wolf 我的意思是相同的非限定名称,当然。注释无法很好地显示代码,但我已经写了一个外部示例Derived2无法隐藏mustRemainBase - Angew is no longer proud of SO
显示剩余3条评论

6
如果你很着急,可以跳到结论部分查看。在虚函数声明中,只能使用override和final关键字。这两个关键字可以在同一个函数声明中使用,但是否有用取决于具体情况。
以下代码为例:
#include <iostream>
using std::cout; using std::endl;

struct B {
  virtual void f1() { cout << "B::f1() "; }
  virtual void f2() { cout << "B::f2() "; }
  virtual void f3() { cout << "B::f3() "; }
  virtual void f6() final { cout << "B::f6() "; }
  void f7() { cout << "B::f7() "; }
  void f8() { cout << "B::f8() "; }
  void f9() { cout << "B::f9() "; }
};

struct D : B {
  void f1() override { cout << "D::f1() "; }
  void f2() final { cout << "D::f2() "; }
  void f3() override final { cout << "D::f3() "; }  // need not have override
  // should have override, otherwise add new virtual function
  virtual void f4() final { cout << "D::f4() "; }
  //virtual void f5() override final;  // Error, no virtual function in base class
  //void f6(); // Error, override a final virtual function
  void f7() { cout << "D::f7() "; }
  virtual void f8() { cout << "D::f8() "; }
  //void f9() override;  // Error, override a nonvirtual function 
};

int main() {
  B b; D d;
  B *bp = &b, *bd = &d; D *dp = &d;
  bp->f1(); bp->f2(); bp->f3(); bp->f6(); bp->f7(); bp->f8(); bp->f9(); cout << endl;
  bd->f1(); bd->f2(); bd->f3(); bd->f6(); bd->f7(); bd->f8(); bd->f9(); cout << endl;
  dp->f1(); dp->f2(); dp->f3(); dp->f6(); dp->f7(); dp->f8(); dp->f9(); cout << endl;
  return 0;
}

输出结果为:
B::f1() B::f2() B::f3() B::f6() B::f7() B::f8() B::f9()
D::f1() D::f2() D::f3() B::f6() B::f7() B::f8() B::f9()
D::f1() D::f2() D::f3() B::f6() D::f7() D::f8() B::f9()
  1. 比较 f1()f6()。我们知道,overridefinal 在语义上是独立的。

    • override 意味着该函数覆盖了其基类中的虚函数。参见 f1()f3()
    • final 意味着该函数不能被其派生类覆盖。(但该函数本身不需要覆盖基类的虚函数。)参见 f6()f4()
  2. 比较 f2()f3()。我们知道,如果一个成员函数在没有使用 virtual 的前提下使用了 final,则它已经覆盖了基类中的虚函数。在这种情况下,关键字 override 是多余的。

  3. 比较 f4()f5()。我们知道,如果一个成员函数使用了 virtual,并且它不是继承层次结构中的第一个虚函数,则我们应该使用 override 来指定覆盖关系。否则,我们可能会意外地在派生类中添加新的虚函数。

  4. 比较 f1()f7()。我们知道,任何成员函数,不仅包括虚函数,都可以在派生类中被覆盖。而 virtual 指定的是多态性,这意味着决定运行哪个函数的决策被延迟到运行时而不是编译时。(在实践中应该避免这样做。)

  5. 比较 f7()f8()。我们知道,甚至可以覆盖一个基类函数并将其变为一个新的虚函数。(这意味着从 D 派生的任何成员函数 f8() 都将是虚函数。)(在实践中也应该避免这样做。)

  6. 比较 f7()f9()。我们知道,override 可以在要覆盖派生类中的虚函数时帮助我们找到错误,而忘记在基类中添加关键字 virtual

总之,我认为最佳实践是:

  • 只有在基类的第一个虚函数声明中使用 virtual
  • 除非指定了 final,否则始终使用 override 来指定覆盖虚函数。

5
下面的代码(带有final修饰符)可以编译。但是当将final替换为override final时,编译会失败。因此,override final传达了比只有final更多的信息(并防止编译)。
class Base
{
public:
    virtual ~Base() {}
};

class Derived : public Base
{
public:
    virtual void foo() final
    {
        std::cout << "in Derived foo\n";
    }
};

基本上,override final表示此方法不能被任何派生类重写,并且此方法覆盖了基类中的虚拟方法。final单独使用时不指定基类的覆盖部分。


1
"final关键字本身并不指定基类的重写部分。" 但是,void foo() final可以实现这一点。 - Johan Lundberg

1
< p >“final”并不一定意味着“override”。事实上,您可以声明一个立即声明为“final”的“virtual”函数在此处查看。 “final”关键字只是说明没有派生类可以创建此函数的覆盖。

“override”关键字非常重要,因为它强制执行实际上是覆盖虚拟函数(而不是声明新的无关函数)。请参见有关“override”的此篇文章

长话短说,它们各自具有特定的目的,通常同时使用是正确的。


3
嗯,我希望你所发布的第一个示例不能编译通过,因为在我的理解中,声明虚函数并同时将其定义为最终版本是完全没有意义的。 - antred
2
C++ 核心指南建议仅使用一个 https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rh-override - Chris Guzak

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