《Thinking in C++》volume 2第五章有这么一句话:
Member template functions cannot be declared virtual.Current compiler technology experts to be able to determine the size of a class’s virtual function table when the class is parsed.Allowing virtual member template functions woule require knowing all calls to such member functions everywhere in the program ahead of time.This is not feasible,especially for multi-file projects.
在这段话里作者解释了为什么类的成员模板函数不能是虚函数。自己半懂不懂。没写过编译器,遇到费解的概念如编译时/运行时、静态/动态、内部连接/外部连接,以及很多比如“函数模板不能有默认的模板参数”这样的规则时,只好望洋兴叹。
然而问题不可累积。有必要借助这个问题明确一些基本的概念了。
首先:当模板类被实例化时,它实际上也产生了一种新的类型,不同的参数实例化出不同的类型。例如,用五个不同的参数分别实例化一个模板类,则相当于新定义了五个普通类。用代码说明问题:
- #include <iostream>
- #include <typeinfo>
- using namespace std;
- template <class Type> class Test {
- public:
- void f() {
- cout << "My Type is: " << typeid(*this).name() << endl;
- }
- };
- int main() {
- Test<char>().f();
- Test<int>().f();
- cout.setf(ios::boolalpha);
- cout << bool(typeid(Test<char>) == typeid(Test<int>)) << endl;
- return 0;
- }
输出结果:
My Type is: class Test<char>
My Type is: class Test<int>
false
由此可知,这是两种完全不同的类型,它们的差别就像int和char的差别。顺便,如何实现这两种类型的转换呢?《Thinking in C++》volume 2告诉了我们一种技术:模板拷贝构造函数。代码示例:
- #include <iostream>
- #include <typeinfo>
- using namespace std;
- template <class Type> class Test {
- public:
- void f() {
- cout << "My Type is: " << typeid(*this).name() << endl;
- }
- };
- int main() {
- Test<char> tc;
- Test<char> tc2;
- tc2 = tc; //ok
- Test<int> ti;
- ti = tc; // error :there is no acceptable conversion
- return 0;
- }
上述代码直接将Test<char>类型的变量赋值给Test<int>类型的变量编译时会出现错误。不出错的方法就是使用模板拷贝构造函数进行类型转换,如下,把Test类定义成下面这个样子即可:
- template <class Type> class Test {
- public:
- Test() {}
- template <class I> Test(const Test& t) {}
- void f() {
- cout << "My Type is: " << typeid(*this).name() << endl;
- }
- };
其二:在这一点上,模板函数与模板类有一样的特点。即:不同的参数实例化出不同的重载函数。代码演示:
- #include <iostream>
- #include <typeinfo>
- using namespace std;
- template <class T> T add(const T& a, const T& b) {
- return a+b;
- }
- int main() {
- cout << typeid(add<int>).name() << endl;//int __cdecl(int const &,int const &)
- cout.setf(ios::boolalpha);
- cout << bool(typeid(add<int>) == typeid(add<double>)) << endl;//false
- add(3, 4); // add<int>(3,4), address: 004171C0
- add(3.0, 4.0); // add<double>(3.0, 4.0), address: 00417200
- return 0;
- }
那么,又引出两个问题:关于重载以及函数指针。
先看重载。C++编译器不会用返回值来重载函数。然而上面的例子说明不同的类型参数实例化出来不同的重载函数。且看看仅把模板类型参数作为模板函数的返回值时用不同的参数还能不能实例化出来不同的函数:
- #include <iostream>
- #include <typeinfo>
- using namespace std;
- template <class T> T onlyRet(int a) {
- cout << a << endl;
- return T();
- }
- int main() {
- cout.setf(ios::boolalpha);
- cout << bool(typeid(onlyRet<char>) == typeid(onlyRet<short>)) << endl;
- return 0;
- }
输出仍然为false。由此可见:模板函数提供了一种实现特殊重载的编码方法,但跟重载是两回事。(说是特殊的重载,因为仅仅是类型替换性质的,而真正的重载函数彼此之间的内部实现可以完全不同)。可以生成该程序的汇编代码看看编译后的函数名字修饰,在命令行下:cl /Fatemp.asm /FAs template_test.cpp,然后打开temp.asm,搜索onlyRet,可以看到如下一段代码:
PUBLIC ??$onlyRet@F@@YAFH@Z ; onlyRet<short>
PUBLIC ??$onlyRet@D@@YADH@Z ; onlyRet<char>
另一个问题就是函数指针的问题。既然模板可以接受任意类型作为参数,那么如果有模板函数指针的话,它是不是就可以指向任意一个有指定参数数目的函数了?还是用代码来说话:
- #include <iostream>
- #include <typeinfo>
- using namespace std;
- template <class T> void (*p)(T);
- void f(int a) {}
- int main() {
- p = f;
- return 0;
- }
很可惜,上面的
template <class T> void (*p)(T);
这一句在编译时会得到一个错误:error C2998: 'void (__cdecl *__cdecl p)(T)' : cannot be a template definition。我想,这可能是”type deduction”的问题,也许高手不用测试程序就能推测出来,但不管怎样,自己也明白了这样不行。
现在,就可以明白作者的解释了。作者说:当前的编译器都期望在处理类的定义的时候就能确定这个类的虚函数表的大小,如果允许有类的虚成员模板函数,那么就必须要求编译器提前知道程序中所有对该类的该虚成员模板函数的调用,而这是不可行的。
为什么作者这样说呢?从上面的演示知道,对于一个模板函数,不同的模板参数会产生出不同的函数。这样的话,如果要知道类内包含多少个虚函数,就只能去代码中寻找。这就不单单是多文件的问题,还有RTTI的问题了。