0%

cpp

腾讯面试中被问到一个很有意思的面试题 虚函数的模板能被实例化吗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename T>
class weight
{
public:
void foo1(T a) { ++a; }
virtual void foo2(T b) { int temp = *b; }
};
int main()
{
// 编译失败
weight<int>* par=new weight<int>;
par->foo1(3);
return 0;

}

实验结果是不行的

显然模板虚函数是编译不过的,至于为什么,我们可以深究至C++多态的实现原理,就能知道为什么C++不允许定义模板虚函数了。

我们知道C++的多态是通过虚表实现的,对于含有虚函数的类,会为其定义一个虚表,每个实例化的对象都有一个指向该虚表的指针,所以同样的类,含有虚函数的类的实例大小比不含虚函数的多上一个指针的大小,虚表里为每个虚函数维护着一条跳转记录,这些跳转地址在编译期就被确定了,存放在类定义模块的数据段中,在程序运行期是不可修改的。那么这跟模板虚函数有什么关系呢?

让我们了解一点关于模板的特性,C++对于模版的处理,首先,模版并不算一种类型,在编译时,编译器只对已经实例化的模板类生成对应的模板类代码,假如这些类中定义的有模板类虚函数,则对每个实例化的模板类型创建一个虚表,这就是第一种情况—模板类虚函数,是可行的。

现在再看看模板虚函数,为什么不可行,就拿上面的代码讲:

A是一个类型,它含有模板虚函数,虽然是虚函数,但是函数的符号并不确定,因为我们不知道模板T是一个什么类型,对于从没调用过这个模板函数的情况下,这个模板虚函数甚至都不会实例化,那么就相当于没有虚函数了。那么为了实现模板虚函数,我们姑且认为它就是含有虚函数,所以A应该有一张虚表,但是A的虚函数符号并不确定,要根据当前调用的情况来确定,A的这个模板虚函数到底实例化了几个类型,那么对于每个类型的虚函数都添加一个虚表记录,这样看起来,实现模板虚函数貌似是可行的,但是这也只仅限于单个文件编译成可执行文件的情况下。

我们都知道C++编译中间是有几个步骤的,预处理、编译和链接,每个cpp或c文件都会被编译成目标文件,然后这些目标文件在通过链接生成可执行文件。那么考虑一下这种情况,假如现在我有两个cpp文件分别是x.cpp和y.cpp,上面的模板虚函数,我在x.cpp文件中实例化了

1
2
void foo(int& t);
void foo(float& t);

而在y.cpp中实例化了

1
2
void foo(int& t);
void foo(bool& t);
1
2
3
4
5
那么x.o和y.o中的A类的虚表都含有两天记录,但是函数符号却并不一样,那么为了实现模板虚函数,进行链接的时候就需要对虚表合并去重了,先抛去实现代价的问题,从理论上看起来的确是可行的。

然而事情并不是到此为止了,我们知道目标文件不只是可以链接成可执行文件,还可以链接成静态库和动态库,对于静态库,再进行链接的时候和普通链接差别不大,但是动态库就没有那么好运了。

考虑这样的一种情况,在动态库里面定义了上面的模板函数,而且实例化了
1
void foo(bool& t);

那么此时为了继续下去,就得修改动态链接库中B类的虚表了,为它添加一条记录,很显然是行不通的。至于为什么行不通,抛开程序段的可读写的问题不谈,如果真的可修改,那么这个类型的每个实例都可能会守到其它实例的影响了,与类的设计原则相悖了。

至此为什么模板虚函数为什么行不通已经很明显了。

总结 动态链接库的原因,违反设计