提问人:fredoverflow 提问时间:7/29/2012 最后编辑:iBugfredoverflow 更新时间:10/18/2022 访问量:28576
最终虚拟功能的意义何在?
What's the point of a final virtual function?
问:
维基百科在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
};
我不明白引入虚拟功能并立即将其标记为最终功能的意义。这只是一个坏例子,还是还有更多?
答:
这对我来说似乎一点用处都没有。我认为这只是一个演示语法的例子。
一个可能的用途是,如果你不希望 f 真正被覆盖,但你仍然想生成一个 vtable,但这仍然是一种可怕的做事方式。
评论
dynamic_cast
我不明白引入虚拟功能并立即将其标记为最终功能的意义。
该示例的目的是说明工作原理,它就是这样做的。final
一个实际目的可能是了解 vtable 如何影响类的大小。
struct Base2 {
virtual void f() final;
};
struct Base1 {
};
assert(sizeof(Base2) != sizeof(Base1)); //probably
Base2
可以简单地用于测试平台细节,并且没有必要覆盖,因为它只是为了测试目的,所以它被标记为 .当然,如果你这样做,设计就有问题了。我个人不会创建一个带有函数的类,只是为了检查 .f()
final
virtual
vfptr
对于要标记的函数,它必须是 ,即在 C++11 §10.3 第 2 段中:final
virtual
[...]为了方便起见,我们说任何虚函数都会覆盖自身。
及第4段:
如果某个类 B 中的虚函数 f 用 virt-specifier final 标记,并且位于派生自 B a 函数 D::f 覆盖 B::f,程序格式不正确。[...]
即,仅需要与虚函数(或与阻止继承的类)一起使用。因此,该示例需要用于有效的 C++ 代码。final
virtual
编辑:需要完全清楚的是:“要点”询问了为什么使用虚拟技术的问题。使用它的底线原因是 (i) 因为代码不会以其他方式编译,以及 (ii) 当一个类足够时,为什么要使用更多类使示例更复杂?因此,仅使用一个具有虚拟最终函数的类作为示例。
评论
通常不会用于基类的虚函数定义。 将由重写函数的派生类使用,以防止进一步的派生类型进一步重写该函数。由于重写函数必须是虚的,因此通常意味着任何人都可以在进一步的派生类型中重写该函数。 允许一个函数指定一个函数,该函数覆盖另一个函数,但不能覆盖该函数本身。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 { ... }
};
使用此声明会导致 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 final
Base::test()
struct Derived : Base {
void test(int = 0) { std::cout << "Derived::test()\n"; }
};
无论是否有效,此定义都是有效的,并且代码的行为完全相同。Base::test()
virtual final
Derived
Derived o; o.test(); run(&o);
至于对用户的明确声明,我个人认为不标记方法比标记方法更清楚地向用户声明该方法不打算被覆盖。但我想哪种方式更清晰取决于开发人员阅读代码以及他们熟悉的约定。virtual
virtual final
评论
final
override final
final override
final
override
virtual
override
final
除了上面的好答案之外 - 这是一个著名的 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 很容易实现最终效果。
评论
取而代之的是:
public:
virtual void f();
我发现写这个很有用:
public:
virtual void f() final
{
do_f(); // breakpoint here
}
protected:
virtual void do_f();
主要原因是,在调度到可能被覆盖的实现中的任何一个之前,您现在有一个断点位置。可悲的是(恕我直言),说“最终”也要求你说“虚拟”。
在重构遗留代码时(例如,从母类中删除虚拟方法),这对于确保没有子类使用此虚拟函数非常有用。
// 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() {}
我发现了另一种情况,其中虚拟功能被声明为最终功能是有用的。此案例是 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"
};
virtual
+ final
在一个函数声明中使用,以使示例简短。
关于 和 的语法,维基百科的例子通过引入 Base1 containing 和 Base2 containing 会更有表现力(见下文)。virtual
final
struct Base2 : Base1
virtual void f();
void f() final;
标准
参考 N3690:
virtual
可以成为function-specifier
decl-specifier-seq
final
可以成为virt-specifier-seq
没有规则必须同时使用关键字和具有特殊含义的标识符。第 8.4 节,函数定义(注意 opt = 可选):virtual
final
函数定义:
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 上、...(请参阅编译器资源管理器)。virtual
final
struct A
{
virtual void f() {}
};
struct B : A
{
void f() final {}
};
int main()
{
auto b = B();
b.f();
}
PS:cppreference 上的示例也没有在同一声明中将 virtual 与 final 一起使用。
PPS:这同样适用于。override
下面是你实际上可能选择在基类中同时声明函数的原因:virtual
final
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
评论
virtual
我认为大多数答案都忽略了一个重要的点。 表示在指定后不再表示。在基类上标记它确实几乎毫无意义。final
override
当派生类可能进一步派生时,它可用于将给定方法的实现锁定到它提供的方法。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; }
};
下一个:比较悬空指针是否合法?
评论
int final = 7;