在C ++中,何时应在虚拟方法声明中使用final?


11

我知道该final关键字用于防止虚拟方法被派生类覆盖。但是,当我真的应该在方法中使用关键字时,找不到任何有用的示例。更甚者,使用虚拟方法感觉像是一种难闻的气味,因为它不允许程序员将来扩展类。finalvirtualfinal

我的问题是下一个:

当我真的应该finalvirtual方法声明中使用时,是否有任何有用的情况?


出于所有相同的原因使Java方法成为最终方法?
2015年

在Java中,所有方法都是虚拟的。基类中的final方法可以提高性能。C ++具有非虚拟方法,因此这种语言不是这种情况。您还知道其他情况吗?我想听听他们的声音。:)
metamaker

猜猜我不太擅长解释事情-我想说的是与迈克·纳基斯完全相同的事情。
2015年

Answers:


12

final关键字的功能概述:假设我们有基类A和派生类B。函数f()可以声明Avirtual,表示该类B可以覆盖它。但是,然后类B可能希望任何进一步派生的类B都不能重写f()。那是我们需要在中声明f()as final的时候B。没有final关键字,一旦我们将方法定义为虚拟方法,任何派生类都将可以随意重写它。该final关键字用来杜绝这种自由。

你为什么会需要这样的事情的一个例子:假设类A定义了一个虚函数prepareEnvelope()和类B重写它,实现它的呼叫到自己的虚拟方法的序列stuffEnvelope()lickEnvelope()sealEnvelope()。类B打算允许派生类重写这些虚拟方法以提供其自己的实现,但是类B不想允许任何派生类重写prepareEnvelope()并因此更改填充,舔,密封或忽略调用其中一个的顺序。因此,在这种情况下,类B声明prepareEnvelope()为final。


首先,谢谢您的回答,但这不正是我在问题的第一句话中写的内容吗?我真正需要的是什么情况class B might wish that any class which is further derived from B should not be able to override f()?在现实世界中是否存在这样的类B和方法f()并非常合适?
metamaker

是的,抱歉,等等,我正在写一个示例。
Mike Nakis

@metamaker你去了。
Mike Nakis

1
确实是一个很好的例子,这是迄今为止最好的答案。谢谢!
metamaker'7

1
然后,谢谢您所浪费的时间。我在互联网上找不到很好的例子,浪费了很多时间。我会选择+1作为答案,但由于我的声誉不高,所以我不能这样做。:(也许以后。
metamaker

2

从设计角度来看,通常可以将事物标记为不变。以相同的方式,const提供编译器保护并指示状态不应更改,final可用于指示行为在继承层次结构中不应进一步更改。

考虑一种视频游戏,其中车辆将玩家从一个位置带到另一个位置。所有车辆应在出发前进行检查,以确保其行驶到有效的位置(例如,确保该位置的底座未损坏)。我们可以从使用非虚拟接口习惯用法(NVI)开始,以确保无论使用哪种车辆都可以进行此检查。

class Vehicle
{
public:
    virtual ~Vehicle {}

    bool transport(const Location& location)
    {
        // Mandatory check performed for all vehicle types. We could potentially
        // throw or assert here instead of returning true/false depending on the
        // exceptional level of the behavior (whether it is a truly exceptional
        // control flow resulting from external input errors or whether it's
        // simply a bug for the assert approach).
        if (valid_location(location))
            return travel_to(location);

        // If the location is not valid, no vehicle type can go there.
        return false;
    }

private:
    // Overridden by vehicle types. Note that private access here
    // does not prevent derived, nonfriends from being able to override
    // this function.
    virtual bool travel_to(const Location& location) = 0;
};

现在让我们说我们的游戏中有飞行器,所有飞行器都需要并有一个共同点,那就是它们必须在起飞前经过机库内的安全检查。

在这里,我们可以final用来保证所有飞行器都将通过此类检查,并传达飞行器的设计要求。

class FlyingVehicle: public Vehicle
{
private:
    bool travel_to(const Location& location) final
    {
        // Mandatory check performed for all flying vehicle types.
        if (safety_inspection())
            return fly_to(location);

        // If the safety inspection fails for a flying vehicle, 
        // it will not be allowed to fly to the location.
        return false;
    }

    // Overridden by flying vehicle types.
    virtual void safety_inspection() const = 0;
    virtual void fly_to(const Location& location) = 0;
};

通过final以这种方式使用,我们有效地扩展了非虚拟接口习惯用法的灵活性,以在继承层次结构(甚至是事后考虑到脆弱的基类问题)的范围内为虚拟函数本身提供统一的行为。此外,我们为自己准备了摆动空间,以作事后考虑对所有飞行器类型产生影响的集中更改,而无需修改现有的每个飞行器实现。

这是一个使用的示例final。在某些情况下,对于虚拟成员函数的任何进一步重写都根本没有道理-这样做可能导致脆弱的设计并违反您的设计要求。

final从设计/架构的角度来看,这是有用的。

从优化器的角度来看,它也是有用的,因为它为优化器提供了设计信息,使它可以对虚拟函数调用进行虚拟化(消除了动态调度开销,并且通常更重要的是,消除了调用者和被调用者之间的优化障碍)。

从评论:

为什么将final和virtual同时使用?

对于位于层次结构根部的基类,将函数声明为virtual和都没有任何意义final。这对我来说似乎很愚蠢,因为这会使编译器和人类读者都不得不跳过不必要的麻烦,而virtual在这种情况下,只需避免完全避免就可以避免不必要的麻烦。但是,子类继承虚拟成员函数,如下所示:

struct Foo
{
   virtual ~Foo() {}
   virtual void f() = 0;
};

struct Bar: Foo
{
   /*implicitly virtual*/ void f() final {...}
};

在这种情况下,是否Bar::f显式使用virtual关键字Bar::f都是虚拟函数。virtual在这种情况下,关键字将变为可选。因此,它可能是有意义的Bar::f被指定为final,即使它是一个虚函数(final可以被用于虚拟函数)。

而且有些人可能希望从风格上明确指出它Bar::f是虚拟的,例如:

struct Bar: Foo
{
   virtual void f() final {...}
};

对我来说,在这种情况下(和和)在同一函数中同时使用virtualfinal说明符是多余的,但是在这种情况下,这只是样式问题。某些人可能会发现在这里传达了一些有价值的信息,就像使用带有外部链接的函数声明一样(即使它是可选的,但缺少其他链接限定符)。virtualoverridevirtualextern


您打算如何覆盖类中的private方法Vehicle?你不是说protected
安迪

3
@DavidPacker private不会扩展到覆盖(有点违反直觉)。虚拟函数的公共/受保护/私有说明符仅适用于调用者,不适用于重写器(简而言之)。无论其可见性如何,派生类都可以从其基类覆盖虚拟函数。

@DavidPacker protected可能更直观。我只想尽可能地降低可见度。我认为以这种方式设计语言的原因是,否则,在友谊的上下文之外,私有虚拟成员函数将毫无意义,因为如果访问说明在上下文中很重要,则只有类,而只有朋友可以覆盖它们而不是仅仅调用。

我以为将它设置为private甚至无法编译。例如,Java和C#都不允许这样做。C ++每天让我感到惊讶。甚至经过10年的编程。
安迪

@DavidPacker当我第一次遇到它时,它也使我绊倒了。也许我应该这样做protected以避免混淆别人。最后,我至少发表了一条评论,描述了如何仍然可以重写私有虚拟功能。

0
  1. 它可以进行很多优化,因为在编译时可能会知道调用了哪个函数。

  2. 小心扔掉“代码气味”一词。“最终”并非不可能扩展该类。双击“最终”一词,按退格键,并扩展课程。但是final是一个出色的文档,开发人员不希望您重写该函数,因此下一个开发人员应格外小心,因为如果final方法被这种方法覆盖,则该类可能会停止正常工作。


为什么会finalvirtual曾在同一时间使用?
罗伯特·哈维

1
它可以进行很多优化,因为在编译时可能会知道调用了哪个函数。 您能解释一下或提供参考吗? 但是final是一个出色的文档,开发人员不希望您重写该函数,因此下一个开发人员应格外小心,因为如果final方法被这种方法覆盖,则该类可能会停止正常工作。 那不是难闻的气味吗?
metamaker

@RobertHarvey ABI稳定性。在其他情况下,可能应该是finaloverride
Deduplicator

@metamaker我有同样的Q,在评论这里讨论- programmers.stackexchange.com/questions/256778/... - &只因为要求好笑都有所涉猎到其他几次讨论,!基本上,这是关于优化的编译器/链接器是否可以确定引用/指针是否可能表示派生类并因此需要虚拟调用的问题。如果无法证明该方法(或类)在引用本身的静态类型之外不能被覆盖,则可以取消虚引用:直接调用。如果有任何疑问-经常-他们必须进行虚拟通话。声明final确实可以为他们提供帮助
underscore_d
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.