秒杀面试题:深入final,掌握C 性能优
C 11之后有了final,它用来指定不能在派生类中重写虚函数,或者不能从中派生类。
例如下面这个例子,当添加final之后:
- foo函数被禁止重写
- bar函数被禁止重写
- B类被禁止继承
那么,除了这些还有什么?如果面试官问你final,你只答这些能过?接下来本文来拓展一下final的妙用!
代码语言:javascript复制struct Base
{
virtual void foo();
};
struct A : Base
{
void foo() final; // Base::foo is overridden and A::foo is the final override
void bar() final; // Error: bar cannot be final as it is non-virtual
};
struct B final : A // struct B is final
{
void foo() override; // Error: foo cannot be overridden as it is final in A
};
struct C : B {}; // Error: B is final
虚函数需要在运行时通过vtable进行间接调用,其中会发生分支预测和指令缓存,还会影响内联。因此相比于函数的直接调用,其性能较差。
通过final可以做到去虚拟化,它是一种编译器优化手段,尝试在编译时而非运行时解决函数调用问题。由于在编译时可以确定调用哪个函数,因此便不会存在前面的vtable问题了,从而极大的提升虚函数调用的性能。
1.去虚拟化与原生对比
以下面为例:A有两个接口,f1、f2,子类B继承A重写了这两个接口,f1后面添加了final,f2不变,f1与f2函数各自调用b的f1与f2。
代码语言:javascript复制class A {
public:
virtual void f1() = 0;
virtual void f2() = 0;
};
class B : public A {
public:
void f1() final override {}
void f2() override {}
};
void f1(B& b) {
b.f1();
}
void f2(B &b) {
b.f2();
}
其汇编代码如下:
代码语言:javascript复制https://gcc.godbolt.org/z/GT78G7T9z
B::f2():
rep ret
f1(B&):
rep ret
f2(B&):
mov rax, QWORD PTR [rdi]
mov rax, QWORD PTR [rax 8]
cmp rax, OFFSET FLAT:B::f2()
jne .L6
rep ret
.L6:
jmp rax
从上面可以看到,对于f1(b)来说由于添加了final,我们可以看到直接调用了B的f1,而并没有加载B的vtable。对于f2(b)来说加载了B的vtable、比较虚函数地址,判断是否jmp。
上面这个例子比较简单,指针加载和跳转的成本可能看起来并不多,因为它只是几条指令,但是这可能涉及分支预测错误和缓存未命中问题,这可能会引发性能下降。
2.总结一下final
final在日常面试/工作中还是比较有用,如果面试官问你final,你可以解答如下。
- 禁止继承
- 禁止虚函数重写
- 去虚拟化提升性能
- 设计约束,禁止子类继承,放置子类重写
总的来说,final
关键字在 C 中用于阻止继承和虚函数重写,提高代码的性能、可维护性和安全性。使用 final
能够更加明确地表达代码的意图,并防止一些潜在的错误。