C ++ 11中“ final”关键字对函数的作用是什么?


143

什么是目的final在C ++ 11的功能关键字?我知道它可以防止派生类重写函数,但是如果是这种情况,那么将final函数声明为非虚函数还不够吗?我在这里还想念另一件事吗?


30
将您的“最终”函数声明为非虚拟还不够吗? “不,无论您是否使用virtual关键字,覆盖函数都是隐式虚拟的。
ildjarn

13
@ildjarn如果他们不作为超类的虚拟声明,这不是事实,你不能从一个类继承,改造了非虚方法变成一个虚拟的..
丹奥

10
@ DanO我认为您不能覆盖,但是您可以那样“隐藏”方法..这会导致很多问题,因为人们并不打算隐藏方法。
亚历克斯·克雷默

16
@DanO:如果它不是超类中的虚拟对象,则不会“覆盖”。
ildjarn

2
同样,“ 覆盖 ”在此处具有特定含义,即为虚拟函数提供多态行为。在您的示例func中不是虚拟的,因此没有要覆盖的内容,因此也没有要标记为override或的内容final
ildjarn 2015年

Answers:


129

正如idljarn在评论中已经提到的,您所缺少的是,如果您要从基类中覆盖某个函数,那么您可能无法将其标记为非虚拟的:

struct base {
   virtual void f();
};
struct derived : base {
   void f() final;       // virtual as it overrides base::f
};
struct mostderived : derived {
   //void f();           // error: cannot override!
};

谢谢!这就是我所缺少的一点:即,即使您的“叶子”类都打算将其功能标记为虚拟,即使它们打算覆盖功能而不是自己覆盖它们也是如此
lezebulon 2012年

8
@lezebulon:如果超类将函数声明为虚函数,则叶类不需要将函数标记为虚函数。
Dan O

5
如果叶类中的方法在基类中是虚拟的,则它们是隐式虚拟的。我认为,如果缺少此隐式“虚拟”,编译器应发出警告。
亚伦·麦克戴德

@AaronMcDaid:编译器通常会警告代码正确无误,可能会引起混乱或错误。我从未见过有人对这种语言的特殊功能感到惊讶,而这种方式可能会引起任何问题,所以我真的不知道该错误可能有多大用处。相反,忘记了virtual可能会导致错误,和C ++ 11添加override标签的功能,将检测情况,无法编译时是指功能覆盖实际
dribeas大卫-罗德里格斯

1
从GCC 4.9更改说明起:“新型继承分析模块改善了非虚拟化。现在,非虚拟化考虑了匿名名称空间和C ++ 11 final关键字”-因此,它不仅是语法糖,而且还具有潜在的优化优势。
kfsone 2014年

126
  • 这是为了防止类被继承。从维基百科

    C ++ 11还增加了阻止从类继承或仅阻止派生类中的重写方法的功能。这是通过特殊标识符final完成的。例如:

    struct Base1 final { };
    
    struct Derived1 : Base1 { }; // ill-formed because the class Base1 
                                 // has been marked final
  • 它还用于标记虚函数,以防止在派生类中覆盖虚函数:

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

维基百科进一步提出了一个有趣的观点

请注意,语言关键字override也不final是。它们在技术上是标识符;只有在特定的上下文中使用它们时,它们才具有特殊的意义在任何其他位置,它们可以是有效的标识符。

这就是说,允许以下内容:

int const final = 0;     // ok
int const override = 1;  // ok

1
谢谢,但是我忘了提及我的问题与方法中“ final”的使用有关
lezebulon 2012年

您确实提到了它@lezebulon :-)“ C ++ 11中“ final”关键字对于函数的目的是什么”。(我的重点)
亚伦·麦克戴德

您编辑过吗?我看不到任何显示“在x分钟前由lezebulon编辑”的消息。那是怎么发生的?提交后,您也许很快就对其进行了编辑?
亚伦·麦克戴德

5
@Aaron:发布后五分钟内所做的编辑不会反映在修订历史记录中。
ildjarn

@Nawaz:为什么它们不只是说明符?是否由于兼容性原因而造成,意味着C ++ 11之前的现有代码可能将final&overlay用于其他目的?
毁灭者

45

“最终”还允许编译器优化绕过间接调用:

class IAbstract
{
public:
  virtual void DoSomething() = 0;
};

class CDerived : public IAbstract
{
  void DoSomething() final { m_x = 1 ; }

  void Blah( void ) { DoSomething(); }

};

使用“ final”,编译器可以CDerived::DoSomething()直接从内部调用Blah(),甚至可以内联。没有它,它必须在内部生成一个间接调用,Blah()因为Blah()可以在已重写的派生类中调用它DoSomething()


29

没有什么可以添加到“最终”的语义方面。

但是我想补充一下克里斯·格林的评论,即“最终” 在不久的将来可能会成为非常重要的编译器优化技术。他不仅提到了简单的情况,而且还提到了可以通过“ final”“关闭”的更复杂的现实世界类层次结构,从而使编译器可以比通常的vtable方法生成更有效的调度代码。

vtables的一个主要缺点是,对于任何这样的虚拟对象(假设在典型的Intel CPU上为64位),仅指针本身就占用了缓存行的25%(64个字节中的8个)。在我喜欢编写的应用程序中,这非常痛苦。(从我的经验来看,从纯粹的性能角度(即C程序员),它是反对C ++的#1论点。)

在要求极端性能的应用程序中(对于C ++而言并不罕见),这的确可能变得非常棒,不需要手动以C样式或怪异的模板处理解决此问题。

这种技术称为去虚拟化。一个值得记住的术语。:-)

Andrei Alexandrescu最近发表了一篇精彩的演讲,很好地解释了您今天如何解决这种情况以及将来“最终裁决”如何成为将来“自动”解决类似案件的一部分(与听众讨论):

http://channel9.msdn.com/Events/GoingNative/2013/Writing-Quick-Code-in-Cpp-Quickly


23
8是64的25%?
ildjarn 2015年

6
有人知道现在可以使用那些编译器吗?
Vincent Fourmond '17

我想说的也是一样。
crazii

8

Final不能应用于非虚拟功能。

error: only virtual member functions can be marked 'final'

能够将非虚拟方法标记为“最终”并不是很有意义。给定

struct A { void foo(); };
struct B : public A { void foo(); };
A * a = new B;
a -> foo(); // this will call A :: foo anyway, regardless of whether there is a B::foo

a->foo()会一直打电话A::foo

但是,如果A :: foo是virtual,则B :: foo将覆盖它。这可能是不希望的,因此使虚拟函数最终化是有意义的。

问题是,为什么要允许最终使用虚拟功能。如果您的层次结构很深:

struct A            { virtual void foo(); };
struct B : public A { virtual void foo(); };
struct C : public B { virtual void foo() final; };
struct D : public C { /* cannot override foo */ };

然后,final将“覆盖范围”设置为可以进行多少次覆盖。其他类可以扩展A和B并覆盖它们的foo,但是如果一个类扩展C则不允许这样做。

因此,使“ top-level” foo final变得没有意义,但从下往下有意义。

(尽管如此,我仍然可以将final和override扩展到非虚拟成员。但是,它们会有不同的含义。)


感谢您的示例,这是我不确定的事情。但仍然:具有最终(虚拟)功能的意义何在?基本上,您将永远无法使用该函数是虚拟的这一事实,因为它无法被覆盖
lezebulon 2012年

@lezebulon,我编辑了我的问题。但是后来我注意到DanO的答案-这是我要说的很清楚的答案。
亚伦·麦克戴德

我不是专家,但是我觉得有时候做一个顶层函数可能有意义final。例如,如果您知道要所有Shapes- foo()某种预定义和确定的值,则任何派生形状都不应修改。还是我错了,在这种情况下可以采用更好的模式吗?编辑:哦,也许是因为在这种情况下,不应该让顶层foo() virtual成为开始?但尽管如此,它可以被隐藏,即使正确调用(多态)通过Shape*...
安德鲁畅

8

我喜欢的'final'关键字的用例如下:

// This pure abstract interface creates a way
// for unit test suites to stub-out Foo objects
class FooInterface
{
public:
   virtual void DoSomething() = 0;
private:
   virtual void DoSomethingImpl() = 0;
};

// Implement Non-Virtual Interface Pattern in FooBase using final
// (Alternatively implement the Template Pattern in FooBase using final)
class FooBase : public FooInterface
{
public:
    virtual void DoSomething() final { DoFirst(); DoSomethingImpl(); DoLast(); }
private:
    virtual void DoSomethingImpl() { /* left for derived classes to customize */ }
    void DoFirst(); // no derived customization allowed here
    void DoLast(); // no derived customization allowed here either
};

// Feel secure knowing that unit test suites can stub you out at the FooInterface level
// if necessary
// Feel doubly secure knowing that your children cannot violate your Template Pattern
// When DoSomething is called from a FooBase * you know without a doubt that
// DoFirst will execute before DoSomethingImpl, and DoLast will execute after.
class FooDerived : public FooBase
{
private:
    virtual void DoSomethingImpl() {/* customize DoSomething at this location */}
};

1
是的,这实质上是模板方法模式的示例。在C ++ 11之前,我一直希望TMP像Java一样,希望C ++具有诸如“ final”之类的语言功能。
Kaitain '17

6

final 添加一个明确的意图是不覆盖您的函数,如果违反此规则,则会导致编译器错误:

struct A {
    virtual int foo(); // #1
};
struct B : A {
    int foo();
};

如代码所示,它会编译并B::foo覆盖A::fooB::foo顺便说一句,它也是虚拟的。但是,如果将#1更改为virtual int foo() final,则这是编译器错误,并且不允许我们A::foo在派生类中进一步覆盖任何内容。

请注意,这不允许我们“重新打开”新的层次结构,即无法创建B::foo新的,不相关的功能,这些功能可以独立于新的虚拟层次结构的开头。一旦函数是最终函数,就永远不能在任何派生类中再次声明它。


5

final关键字允许您声明一个虚拟方法,将其重写N次,然后强制执行“不能再重写”。这在限制使用派生类时很有用,这样您可以说:“我知道我的超类允许您重写此类,但是如果您想从我派生,则不能!”。

struct Foo
{
   virtual void DoStuff();
}

struct Bar : public Foo
{
   void DoStuff() final;
}

struct Babar : public Bar
{
   void DoStuff(); // error!
}

正如其他张贴者指出的那样,它不能应用于非虚拟功能。

final关键字的一个目的是防止方法的意外覆盖。在我的示例中,DoStuff()可能是一个辅助函数,派生类只需重命名即可获得正确的行为。没有final,直到测试才能发现错误。


1

将C ++中的final关键字添加到函数后,可以防止基类覆盖它。另外,当添加到类中时,可以防止任何类型的继承。考虑以下示例,该示例显示了最终说明符的用法。该程序编译失败。

#include <iostream>
using namespace std;

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

int main()
{
  Derived d;
  Base &b = d;
  b.myfun();
  return 0;
}

也:

#include <iostream>
class Base final
{
};

class Derived : public Base
{
};

int main()
{
  Derived d;
  return 0;
}

0

马里奥·克涅佐维奇的答案的补充:

class IA
{
public:
  virtual int getNum() const = 0;
};

class BaseA : public IA
{
public:
 inline virtual int getNum() const final {return ...};
};

class ImplA : public BaseA {...};

IA* pa = ...;
...
ImplA* impla = static_cast<ImplA*>(pa);

//the following line should cause compiler to use the inlined function BaseA::getNum(), 
//instead of dynamic binding (via vtable or something).
//any class/subclass of BaseA will benefit from it

int n = impla->getNum();

上面的代码显示了理论,但并未在实际的编译器上进行实际测试。如果有人粘贴分解后的输出,将不胜感激。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.