C++11引入了关键字final,按官方的标准是该关键字是用来标识虚函数不能在子类中被覆盖(override),或一个类不能被继承。用法如下:
struct Base
{
virtual void foo();
};
struct A : Base
{
void foo() final; // Base::foo 被覆盖而 A::foo 是最终覆盖函数
void bar() final; // 错误:非虚函数不能被覆盖或是 final
};
struct B final : A // struct B 为 final
{
void foo() override; // 错误:foo 不能被覆盖,因为它在 A 中是 final
};
struct C : B // 错误:B 为 final
{
};
然而在除了上述的标准化作用之外,或许可以从该关键字本身来挖掘更多可能性,如性能提升。
在正式开始本话题之前,我们来回顾一下C++中的动态绑定(dynamic binding),动态绑定依赖于虚表(vtable)来在运行时决定需要被调用的方法/函数,即通过对象指向的vtable来查找调用的函数。
struct A : Base
{
virtual void foo() override;
};
A * obj = new A();
a->foo();
其中a->foo()
先首先根据a指向的虚表找到foo的函数地址,然后再执行foo函数的调用,因此这里就有一定性能开销,即从vtable中找到foo的地址。
为什么要从vtable中找foo的地址,这里因为A有可能被其它类继承,在子类中覆盖了foo的实现,也即为了实现动态绑定。
思考一个问题,如果明确知道A不会被其它类继承的情况下,是不是可以省掉从vtable中找到foo地址的开销呢?答案是肯定的,如果明确知道该信息,完全可以将A类型上的调用全部退化成对A上非virtual函数的调用。因此如果一个类或函数声明成final,在编译器层面可以优化掉一步vtable的查找开销,这在C++的标准中并没有规定,完全依赖于编译器的实现。
即然依赖于编译器的实现,那是否有编译器实现了这种优化呢?下面来看一下Clang的编译结果。
class Base
{
public:
__attribute__((noinline)) virtual float GetA( int i ) {
return i * float(i);
}
__attribute__((noinline)) virtual float Get( int i ) {
return i * float(i);
}
};
class Derived_A : public Base
{
public:
__attribute__((noinline))virtual float Get( int i ) {
return i * float(i) * 2.1f;
}
};
class Derived_B final : public Base
{
public:
__attribute__((noinline)) virtual float Get( int i ) {
return i * float(i) * 3.4f;
}
};
__attribute__((noinline))float Test_A(Derived_A* a, int i)
{
return a->Get(i) + 1.0f;
}
__attribute__((noinline))float Test_B(Derived_B* a, int i)
{
return a->Get(i) + 2.0f;
}
int main(int argc)
{
Derived_A* a = new Derived_A();
auto b = new Derived_B();
auto v1 = Test_A(a, argc);
auto v2 = Test_B(b, argc);
return v1 + v2;
}
由于测试代码非常简单,因此在clang编译时使用O2级别的优化,会导致一些代码被inline,因此在编译时强制不内联函数。Test_A和Test_B的声明也是为了增加一下间接性。
编译后的结果片断如下:
Test_A(Derived_A*, int): # @Test_A(Derived_A*, int)
# %bb.0:
push rax
mov rax, qword ptr [rdi]
call qword ptr [rax + 8]
addss xmm0, dword ptr [rip + .LCPI0_0]
pop rax
ret
# -- End function
.LCPI1_0:
.long 1073741824 # float 2
Test_B(Derived_B*, int): # @Test_B(Derived_B*, int)
# %bb.0:
push rax
call Derived_B::Get(int)
addss xmm0, dword ptr [rip + .LCPI1_0]
pop rax
ret
从编译后的结果可以看出对Derived_A类型的调用Get函数会去查找vtable,而对Derived_B的Get函数调用是在编译时直接使用函数地址。测试显示,直接将Get声明为final可能达到相同的效果。