第三章 虚函数
如果类X定义了一个虚函数或是它派生于这样的类,那么就会由编译器产生一个虚函数表vbtl。虚函数表拥有为该指定类所定义的所有虚函数的指针。每个类有一个虚函数表,该类的每个对象都有一个隐藏的指向该表的指针。之所以是隐藏的,是因为只有编译器才知道vptr在对象内部的偏移量。编译器在对系那个的构造函数中插入代码以正确的初始化vptr。
虚函数的开销包括:
· 1必须在构造函数内初始化vptr
· 2虚函数是通过指针间接调用的。我们必须先得到指向函数表的指针,然后访问正确的函数偏移量。
· 3内联是编译阶段的选择。由于虚函数的类型判断发生在运行时,所以编译器不能内联虚函数。(无法内联虚函数是虚函数最大的性能损失)
头两条不应该视作性能损失,因为不管以何种方式,即使我们不使用动态绑定,都将不得不付出代价。构造函数设置vptr的开销等价于我们初始化类成员的开销,然而初始化类成员是必不可少的;同样我们如果不用多态,那么也需要用一个类似swith的语句,进行判断到底我们调用哪个类(这里的类仍是指在同一个继承层次中的,只不过没有用到虚函数——自然就没有动态绑定)的哪个方法,其代价与第二条的代价是相等的,因此说前两条都是不可改进的,不能称为性能损失。所以,我们可以通过改进第三条改进程序的性能。我们也可以说,对于评价虚函数的性能损失等价于评估无法内联该函数所造成的损失。
然而,由于调用虚函数的对象,无法在编译阶段决定,必须在运行的过程中决定,这个事实让编译器握法进行内联。因为内联是一种发生在编译阶段的选择。但是,我们也有一些办法可以带来更好的新能:
(书中的以下方法是用实现线程安全的实例讲解的)
1 硬编码
即把我们的类中,加入其需要的在派生层次中的某个类。
例如我们的类M,还有一个派生层次类A/B/C/D,A是基类。如果我们使用动态绑定,可以在M中定义一个还有参数A *base方法MyFunc,
void MyFunc(A *base){
...
base.function();
...
}
然后根据具体传进来的是哪个类,我们决定调用哪个function()。我们现在要消除动态绑定:
假设我们的类M就需要用到类B中的function()方法,那么我们直接就把B *base作为类M的一个成员即可。
void MyFunc(){
...
base.function();
...
}
这样的话,我们的代码不仅清楚了动态绑定,还可以让我们的代码内联,或者让编译器帮助我们内联优化。
2 继承 无疑,这种用多态实现的方法,需要到运行时才能确定,因此无法内联;
3 模板 具有了重用和效率两个优点
模板避免了虚函数的使用;
模板可以在声明时,实例化我们需要用到的继承层次中的相应类型,因此可以像硬编码那样——在编译期间确定具体类型,因而可以内联。