寫過模板的朋友也許知道,一個模板程式,當編譯器看到模板定義時並不立即產生代碼,只有在我們用到模板,並對其執行個體化的時候,才會產生特定的執行個體。此時,編譯器就要訪問定義模板的原始碼了。如果原始碼不可訪問,當然,編譯器會報錯的。記得我初學的時候,採用的是直接將聲明和實現全部放在一個.h裡面這個方法。但是,有時候我們確實想在.h檔案中聲明,在CPP檔案中實現,從而實現檔案分離。那麼下面我就寫寫一般模板函數,模板類,模板特化的檔案分離,我自己的心得。
在《C++primer》 中講解了C++ 編譯模板代碼的兩種模型 :(1)包含編譯 (2)分別編譯
(1)包含編譯模型:可以通過在聲明函數模板或類模板的標頭檔中添加一條#include指示使定義可用,從而引入包含相關定義的源檔案
// header file utlities.h#ifndef UTLITIES_H#define UTLITIES_Htemplate <class T> int compare(const T&, const T&);......#include "utilities.cpp"#endif
//implementation file utlities.cpptemplate <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_CPPtemplate<>void Work<int>( int t1){string txt = "now Work is special version.";Display<char>(txt);}#endif
這樣也能通過編譯,也許你覺得第二個辦法不高明。轉來轉去又回到了原點,所以要是不喜歡的話,還是推薦用第一種方法。
綜上所述,我個人覺得,包含編譯在模板程式中的確是首選,他很簡單方便。(分別編譯暫且不談,因為不是每一個編譯器都支援這種方式。)但是採用包含要注意有些特殊情況,如我上面例舉出的例子。