最近在仿寫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(); }
這兩種方法實際上都是包含編譯,沒有本質的區別,不過感覺第二種方法看起來比較舒服。還有一種比較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中就廢除掉了,所以這里就不提了。