写过模板的朋友也许知道,一个模板程序,当编译器看到模板定义时并不立即产生代码,只有在我们用到模板,并对其实例化的时候,才会产生特定的实例。此时,编译器就要访问定义模板的源代码了。如果源代码不可访问,当然,编译器会报错的。记得我初学的时候,采用的是直接将声明和实现全部放在一个.h里面这个方法。但是,有时候我们确实想在.h文件中声明,在CPP文件中实现,从而实现文件分离。那么下面我就写写一般模板函数,模板类,模板特化的文件分离,我自己的心得。
在《C++primer》 中讲解了C++ 编译模板代码的两种模型 :(1)包含编译 (2)分别编译
(1)包含编译模型:可以通过在声明函数模板或类模板的头文件中添加一条#include指示使定义可用,从而引入包含相关定义的源文件
// header file utlities.h #ifndef UTLITIES_H #define UTLITIES_H template <class T> int compare(const T&, const T&); ...... #include "utilities.cpp" #endif//implementation file utlities.cpp template <class T> int compare(const T &v1,const T &v2) { //implemente ...... }
这一策略,实现了头文件和源文件的分离
(2)分别编译: 在分别编译模式下,函数模板的声明被放在头文件中。在这种模式下,函数模板声明和定义的组织方式与程序中的非内联函数的声明和定义组织方式相同。分别编译模型 只是在使用了关键字"export"来告诉编译器模板定义在哪里引用.
如果在头文件类声明中使用了export,则该头文件只能被源文件使用一次;如果在实现文件中使用了export,有下面两种用法
导出类
// XXXXX.H 文件中 定义类 template <typename Type> class Test{/*...*/}; // 在XXXXX.CPP 文件中 export template <typename Type> class Test; #include"XXXXX.h" ...//实现类成员<
导出类成员函数,只用分别对成员使用export.
// XXXXX.H 文件中 只声明 template <typename Type> Type max( Type t1, Type t2 ); // 在XXXXX.CPP 文件中 // 模板定义 export template <typename Type> Type max( Type t1, Type t2 ) {/* . . . */}
看到这儿,你也许心花怒放,似乎觉得如此简单。好,于是你回家敲代码,你会发现,你的编译器VS照样可能告诉你错了!
首先告诉你,一部分原因是编译器问题,VS并不支持分别编译。也就是说,如果你按照上面分别编译的策略来编写代码,编译器会告诉你,目前我们不提供对关键字export的支持,也许将来会提供的支持的。不仅如此 vc的各个版本对C++的标准模板也支持程度也不尽相同,如vc6.0 并不支持模板的部分特化 等...
然后也许你采用的是第一种包含编译,包含编译是所有编译器都支持的,但是你会发现,仍然有错。我们来看下面的一段代码:
在头文件中
#ifndef tmp_h #define tmp_h #include <iostream> #include<vector> using namespace std; template<typename T> class Worker { public: typename vector<T>::size_type sz; T test(const T& t1); }; #include "tmp.cpp" #endif
在CPP文件中
#include"tmp.h" template<typename ch> void Display(string str);//这个函数是我在其他文件中实现的一个函数,不影响我们讨论的结果 template<typename T> T Worker<T>::test(const T& t1) { string str = "worker::test is Running!"; ::Display<char>(str); return t1; }
然后在另一个文件中调用
void main() { Worker<int> w1; w1.test(12.0); _getch(); }
上面的代码满足C++ primer的策略吧,但是编译器 就是报错了:
error C2995: 'T Worker<T>::test(const T &)' : function template has already been defined
因为我们在头文件中显示的包含了相应的CPP文件,但是如果我们多个文件包含了这个头文件,CPP文件也同样会被编译多次。所以出现了上述的那些错误。为了防止包含编译带来的上述错误,我们将CPP文件这样修改下:
#ifndef tmp_cpp #define tmp_cpp #include"tmp.h" template<typename ch> void Display(string str); template<typename T> T Worker<T>::test(const T& t1) { string str = "worker::test is Running!"; ::Display<char>(str); return t1; } #endif
CPP文件也采用预编译命令,防止其重复编译。这样问题就解决了。
也许,你以为就到此结束。编译问题似乎我们完全解决了。但是,另一种情况下,假若我们的模板函数包含一个模板的特化版本,采用这种策略,编译器仍然还是会报错。下面我们来看下面的代码:
//在func.h 中我们声明了几个模板函数(采用如上所述的策略)
#ifndef FUNC_H #define FUNC_H #include <iostream> #include<vector> #include<iterator> #include<algorithm> using namespace std; template<typename ch> void Display(string str); template<typename Type> void Work(Type t1); template<>void Work<int>( int t1);//Work 的特化版本 #include"func.cpp" #endif
//func.cpp实现如下
#ifndef FUNC_CPP #define FUNC_CPP #include "func.h" template<typename ch> void Display(string str) { ostream_iterator<char>out_it(cout, ""); copy(str.begin(), str.end(), out_it); *out_it = '\n'; }; template<typename Type> void Work(Type t1) { string txt = "Work func nomal is runing !"; Display<char>(txt); } template<typename Type,int Num> Type Sum(const Type& t1, int Num) { string txt = "The sum of t1+Num is: "; Type tp = t1+Num; Display<char>(txt); cout<<tp<<endl; return tp; } template<> void Work<int>( int t1) { string txt = "now Work is special version."; Display<char>(txt); } #endif
在另一个CPP文件中
void main() { typedef void (*PTEM)(double t1);//定义一个指针调用一个,非特化版本的模板函数 PTEM pTem = Work; pTem(3232.0); Work<int>(323)//调用特化版本的模板函数 }
看起来没有什么问题吧,但是你编译一下,依然报错。不是么?
error LNK2005: "void __cdecl Work<int>(int)" (??$Work@H@@YAXH@Z) already defined in func.obj
又是重定义!!我们明明都用了#ifndef 这一套预编译指令了的啊,为什么在生成目标文件的时候,还是重定义了。而且只是说特化版本重定义了。其中具体的原因在于特化版本的编译机制,在这里我不想多说,因为本来这个机制比较复杂。我不能在自己都还不是完全理解的情况下,在这里班门弄斧。所以这里只说说我自己的两种处理方法 :
1.依然采用包含编译方法,这种方法最简单。只需要将特化版本的函数,声明称inline函数 即可(其他的不变)。
//头文件中
template<> inline void Work<int>( int t1);
//cpp文件中
template<>
inline void Work<int>( int t1)
{
string txt = "now Work is special version.";
Display<char>(txt);
}
记住啊这里一定要加inline!
2.抛弃包含编译,采用以前的笨办法,将基本模板函数的实现全部放在头文件中,只在头文件中声明特化版本的函数。在CPP文件中只实现特化版本的函数。
#ifndef FUNC_H #define FUNC_H #include <iostream> #include<vector> #include<iterator> #include<algorithm> using namespace std; /////////////////////////头文件中实现基本模板函数//////////////////////// template<typename ch> void Display(string str) { ostream_iterator<char>out_it(cout, ""); copy(str.begin(), str.end(), out_it); *out_it = '\n'; }; template<typename Type> void Work(Type t1) { string txt = "Work func nomal is runing !"; Display<char>(txt); } template<> void Work<int>( int t1);//特化版本的声明 #endif ////////////////////CPP文件中只实现特化版本的函数//////////////// #include "func.h" #ifndef FUNC_CPP #define FUNC_CPP template<> void Work<int>( int t1) { string txt = "now Work is special version."; Display<char>(txt); } #endif
这样也能通过编译,也许你觉得第二个办法不高明。转来转去又回到了原点,所以要是不喜欢的话,还是推荐用第一种方法。
综上所述,我个人觉得,包含编译在模板程序中的确是首选,他很简单方便。(分别编译暂且不谈,因为不是每一个编译器都支持这种方式。)但是采用包含要注意有些特殊情况,如我上面例举出的例子。