最终虚拟功能的意义何在?

What's the point of a final virtual function?

提问人:fredoverflow 提问时间:7/29/2012 最后编辑:iBugfredoverflow 更新时间:10/18/2022 访问量:28576

问:

维基百科在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
};

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

C++ C++11 继承最终 虚函数

评论

3赞 chris 7/29/2012
好吧,Java有它,所以你知道,C++也必须有它。
13赞 Man of One Way 7/29/2012
我想这只是一个不好的例子
3赞 Xeo 7/29/2012
有趣的事实:(几乎)相同的例子可以在§10.3/4下的标准中找到。
3赞 chris 7/29/2012
你总是可以用它来混淆人们,通过使用关键词作为标识符:不过,如果你想听 Stroustrup 的演讲,请看这里int final = 7;
3赞 Nicol Bolas 7/29/2012
“这是一个例子”的哪一部分很难理解?大多数其他示例代码同样毫无意义。示例的目的是展示该功能的工作原理。

答:

11赞 Antimony 7/29/2012 #1

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

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

评论

0赞 VSOverFlow 7/30/2012
最终和虚拟是两个不同的方面。它在重写与重载上下文中变得相关。虚拟限定符表示运行时类型推断。非虚拟意味着编译类型类型推断。当涉及重载,并且涉及类型提升/转换时,非虚拟类型可以产生有趣的结果。但通常鼓励您不要编写此类代码。
0赞 sasha.sochka 8/13/2013
如果编译器不生成 vtable,为什么要生成 vtable?
5赞 Mark Ransom 5/5/2014
@sasha.sochka:例如,我认为没有它就行不通。但是确保 vtable 的通常方法是使析构函数虚拟化。dynamic_cast
6赞 Luchian Grigore 7/29/2012 #2

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

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

一个实际目的可能是了解 vtable 如何影响类的大小。

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

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

Base2可以简单地用于测试平台细节,并且没有必要覆盖,因为它只是为了测试目的,所以它被标记为 .当然,如果你这样做,设计就有问题了。我个人不会创建一个带有函数的类,只是为了检查 .f()finalvirtualvfptr

12赞 Paul Preney 7/29/2012 #3

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

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

及第4段:

如果某个类 B 中的虚函数 f 用 virt-specifier final 标记,并且位于派生自 B a 函数 D::f 覆盖 B::f,程序格式不正确。[...]

即,仅需要与虚函数(或与阻止继承的类)一起使用。因此,该示例需要用于有效的 C++ 代码。finalvirtual

编辑:需要完全清楚的是:“要点”询问了为什么使用虚拟技术的问题。使用它的底线原因是 (i) 因为代码不会以其他方式编译,以及 (ii) 当一个类足够时,为什么要使用更多类使示例更复杂?因此,仅使用一个具有虚拟最终函数的类作为示例。

评论

0赞 Paul Preney 7/29/2012
恭敬地,我完全明白问题的重点。“要点”询问了为什么使用虚拟的问题。使用它的底线原因是因为代码不会以其他方式编译,并且当一个类就足够时,为什么要使用更多类使示例更加复杂?因此,仅使用一个具有虚拟最终函数的类作为示例。QED的。
0赞 Paul Preney 7/29/2012
@Luchian Grigore:我添加了一个编辑以完全清楚,因为我没有用“底线原因”来结束我的答案。也许为什么这个问题会得到如此多的评论,是因为每个人都自然而然地看着这个例子,然后说,“为什么有人会使用那个代码?”而不是从例子作者的角度来看待它。大多数示例都是为了实用而编写的 -- 这个示例只是为了制作一个最小的示例来展示它的行为方式。
79赞 bames53 7/29/2012 #4

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

例如,如果正在设计类层次结构并需要重写函数,但不希望类层次结构的用户执行相同的操作,则可以在派生类中将函数标记为 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 { ... }
};

使用此声明会导致 Derived 的定义产生如下错误:Base::foo()

<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 finalBase::test()

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

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

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

评论

2赞 fredoverflow 7/29/2012
如果隐含的意思,那不是很有意义吗?finaloverride final
5赞 bames53 7/29/2012
我看不出有什么理由不这样做,但我也看不出有任何强有力的理由这样做,因为您已经可以标记函数了。也许应该有一个样式警告,要求所有函数也像其他函数一样标记,并设置样式警告。final overridefinaloverridevirtualoverride
0赞 Trass3r 3/11/2013
接口中的最终方法是“基”类中最终方法的一个例子。此外,如果有人在子类中定义了同名的函数,“final”会产生错误或至少发出警告。问题是编译器是否立即对这种情况进行去虚拟化。
2赞 bames53 3/12/2013
@Trass3r 无法实现接口方法,因为在 C++ 中实现接口方法需要重写。这种无法实现的接口是没有用的。final
0赞 Trass3r 3/12/2013
是的,这就是您在界面中实现它的原因。
8赞 AJed 3/29/2014 #5

除了上面的好答案之外 - 这是一个著名的 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 很容易实现最终效果。

评论

0赞 Troyseph 2/18/2015
这个答案实际上解释了一切,而不是简单地说“否则会是错误的”或“因为它是一个例子”,恕我直言,这应该被接受为这个问题的答案。
1赞 Kevin Hopps 9/28/2016 #6

取而代之的是:

public:
    virtual void f();

我发现写这个很有用:

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

主要原因是,在调度到可能被覆盖的实现中的任何一个之前,您现在有一个断点位置。可悲的是(恕我直言),说“最终”也要求你说“虚拟”。

6赞 Richard Dally 1/19/2017 #7

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

// 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() {}
0赞 dismine 3/31/2017 #8

我发现了另一种情况,其中虚拟功能被声明为最终功能是有用的。此案例是 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"
};
0赞 Roi Danton 5/10/2018 #9

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

关于 和 的语法,维基百科的例子通过引入 Base1 containing 和 Base2 containing 会更有表现力(见下文)。virtualfinalstruct Base2 : Base1virtual void f();void f() final;

标准

参考 N3690

  • virtual可以成为function-specifierdecl-specifier-seq
  • final可以成为virt-specifier-seq

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

函数定义:

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

实践

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

struct A
{
    virtual void f() {}
};

struct B : A
{
    void f() final {}
};

int main()
{
    auto b = B();
    b.f();
}

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

PPS:这同样适用于。override

2赞 Omnifarious 7/27/2018 #10

下面是你实际上可能选择在基类中同时声明函数的原因: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.
};

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

评论

0赞 bobobobo 6/21/2023
final就像,一个函数不一定要打标,其实你应该只指定一个,按照ISOCPPoverridefinalvirtual
0赞 Omnifarious 8/21/2023
@bobobobo - 即使函数从未在父类中标记过,这是否有效?virtual
0赞 p m 10/18/2022 #11

我认为大多数答案都忽略了一个重要的点。 表示在指定后不再表示。在基类上标记它确实几乎毫无意义。finaloverride

当派生类可能进一步派生时,它可用于将给定方法的实现锁定到它提供的方法。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; }
};