c++函数模板声明与定义相分离

最近在仿写stl,发现stl源码中将模板的声明与定义写在一起实在很不优雅。自己尝试用“传统”方法,及在.h文件里声明,在.cpp文件里定义,然后在main函数里包含.h头文件,这样会报链接错误。这是因为函数模板要被实例化后才能成为真正的函数,在使用函数模板的源文件中包含函数模板的头文件,如果该头文件中只有声明,没有定义,那编译器无法实例化该模板,最终导致链接错误。

  上面这句话有点抽象。要理解为什么会出错,首先要理解用传统方法写非模板函数时,编译器是怎么运作的。举个例子

//---------------test.h-------------------// 
 void f();//这里声明一个函数f 
//---------------test.cpp--------------// 
 #include”test.h” 
 void f() 
 { 
 …//do something 
 } //这里实现出test.h中声明的f函数 
//---------------main.cpp--------------// 
 #include”test.h” 
 int main() 
 { 
     f(); //调用f
 } 

编译时会生成两个obj文件,main.obj和test.obj,而在main.obj里并没有f函数的二进制代码,这些代码实际存在于test.obj中。在main.obj中对f的调用只会生成一行call指令,call指令的地址由链接器生成。

再看一个函数模板的例子

//-------------test.h----------------// 
 template<class T> 
 class A 
 { 
    public: 
     void f(); //这里只是个声明 
 }; 
//---------------test.cpp-------------// 
 #include”test.h” 
 template<class T> 
 void A<T>::f() 
 { 
 …//do something 
 } 
//---------------main.cpp---------------// 
 #include”test.h” 
 int main() 
 { 
     A<int> a; 
    a. f(); 
 } 

 我们知道模板有个具现化的过程,在未被使用的时候是不会生成二进制文件的。所以当链接器去找f函数的地址时,因为在这之前没有调用过f(),test.obj里自然就没有f函数的二进制代码,于是就会报错。

要使模板声明与定义分开也不是没有办法。

第一种办法是在main函数里包含cpp文件

//-------------test.h----------------// 
 template<class T> 
 class A 
 { 
    public: 
     void f(); //这里只是个声明 
 }; 
//---------------test.cpp-------------// 
 #include”test.h” 
 template<class T> 
 void A<T>::f() 
 { 
 …//do something 
 } 
//---------------main.cpp---------------// 
 #include”test.cpp” 
 int main() 
 { 
     A<int> a; 
    a. f(); 
 } 

 这样三个文件的内容通过include实际上包含在同一个文件里,自然就不会出错了。同理,还可以这样

//-------------test.h----------------// 
 template<class T> 
 class A 
 { 
    public: 
     void f(); //这里只是个声明 
 }; 
#include<test_impl.h>
//---------------test_impl.h-------------// 
 template<class T> 
 void A<T>::f() 
 { 
 …//do something 
 } 
//---------------main.cpp---------------// 
 #include”test.h” 
 int main() 
 { 
     A<int> a; 
    a. f(); 
 } 

 这两种方法实际上都是包含编译,没有本质的区别,不过感觉第二种方法看起来比较舒服。注意:这里的第二种方法必须把实现文件名.cc改成.h才行,如果仍为.cc文件,则很可能行不通,取决于编译器是否支持。

还有一种比较hack的方法

 

//-------------test.h----------------// 
 template<class T> 
 class A 
 { 
    public: 
     void f(); //这里只是个声明 
 }; 
//---------------test.cpp-------------// 
 #include”test.h” 
 template<class T> 
 void A<T>::f() 
 { 
 …//do something 
 } 
template class A<int>;
//---------------main.cpp---------------// 
 #include”test.h” 
 int main() 
 { 
     A<int> a; 
    a. f(); 
 } 

这样由于在test.cpp里实例化了A<int>,所以链接器能够找到相关代码,就不会报错。但是这样main函数要用哪种类型的模板都得在test.cpp先实例化,很不方便。

以上都是包含编译的方法,其实C++0x有个export关键字,使模板能分离编译,但是基本没有编译器支持,在c++11中就废除掉了,所以这里就不提了。


注意:

上面第三种方法,即显式实例化的方法,如果涉及到命名空间的话,比如A<T>::f()是限定在某个命名空间下,那么实例化template class A<int>的时候也必须处于命名空间以内才有效。如果是在命名空间以外实例化,将会报错。

注意:

如果模板类还有继承关系的话,上面的第三种实例化的方法仍然可行(即便基类有纯虚函数),只需要显式实例化最终的那个子类即可。如果基类含有纯虚函数,且基类的其他函数实现独立放在某个.cc文件中,为了顺利链接,需要在这个.cc文件末尾进行基类的实例化,如:template class Base<2>。 

经测试,这种显式实例化的方法也是奏效的。我原以为可能会因为模板基类是虚类,采用这种方法实例化一个虚类不行,但实践证明仍然可行。

总结起来,把模板类的声明和定义分离开,好处是加快编译速度,但缺点是编译很容易出错,且出错了不好排查。比较有可行性的,要么就是直接把模板类函数的实现与定义同时写到头文件中去,要么就是显式实例化。

猜你喜欢

转载自blog.csdn.net/qq_41230365/article/details/80207819