2012年9月27日 星期四

C++ template使用注意事項

這篇是對上一篇python-like cpp譔寫過程中,遇到的一個疑問。
主因是我在寫一個含有template的Array class,雖然說內容是抄別人的,可是個人自作聰明做了些小修改,結果編譯怎麼樣就是不會過,後來才在google上找到解法,在這裡記錄一下。 我查到最完整的回答是:
http://stackoverflow.com/questions/8752837/undefined-reference-to-template-class-constructor
看到有人的評論是:This has been answered a million times.。我也知道這個問題對比我厲害一點的人都是常識了,可能N年前修資料結構甚至程式設計的時候就被表過了,不過我倒是第一次遇到,大概是從來沒好好的寫過程式吧。
以下就整理一下問題的相關背景、發生原因和解決方式,主要都參考上面的回答。

1. 問題背景:
程式架構其實相當的芭樂,自定義的class Array:Array.h, Array.cpp,記錄一個template類型的動態陣列,會在初始化時排定空間,透過get和set 對其中的元素取值或賦值。
當我在main裡面使用Array a(10),然後將main.cpp, Array.cpp一同編譯時,就出現了下列的錯誤訊息:
main.cpp:(.text+0x1d): undefined reference to `Array::Array(int)'
main.cpp:(.text+0x3d): undefined reference to `Array::set(int const&, int&)'
main.cpp:(.text+0x6f): undefined reference to `Array::get(int const&)'
main.cpp:(.text+0xb8): undefined reference to `Array::get(int const&)'
main.cpp:(.text+0xd8): undefined reference to `Array::~Array()'
main.cpp:(.text+0x183): undefined reference to `Array::~Array()'
總之一切定義、用到的function都找不到。

2. 問題原因:
我們可以把class分為三個階段:Array.h檔內宣告、Array.cpp檔內定義、main.cpp內使用。 這個問題的原因在於,gcc在編譯時並不會同時編譯所有檔案,而是將檔案視為獨立,最後透過linker將上述的檔案連結在一些,消除其中的reference。
template只是一個標註,告訴compiler這個型別還未定型並可置換,當指定如何使用時,才會解開置換並編譯相關的原始碼,compiler在處理Array.cpp時,完全不會編譯Array<int>, Array<float>…等class,除非如下文所示,利用強制的方式指定編譯某個型態;處理main.cpp時,gcc的確在Array.h裡面看到Array的宣告式,因此它將Array的function設為reference,讓linker去尋找相對應的執行碼。
最後linker要連結時就發生問題了,main.cpp要求Array的code,但在Array.cpp裡面卻沒有,就成了undefined reference。

3. 解決方法:
這個問題有三個可行的解決方法:
第一是不要像我一樣傻傻的把class分成兩個檔案,而是直接將宣告和定義寫在相同的.h裡,如此一來,編譯main.cpp時遇到Array即可在Array.h裡面找到對應的template code並編譯。
第二種解決方法,是在Array.cpp的檔案末端,明確的告訴編譯器,這裡有哪些型別的class需要編譯:
template class Array;
template class Array;
… 
產生的Array.o內部,就有Array編譯完成的code供連結。
第三種方法是直接在使用的地方#include Array.cpp

三種方法各有優劣。
第一種方法容易加大原始碼的大小,並把定義和宣告寫在同一個檔案內,也會增加編譯時的成本。
第二種方法的彈性較低,寫了哪些就只能用哪些,對於大多數的STL物件或許還OK,如果程式中有自定義的class就成了大問題;現今STL為了支援泛型,都是用第一種方法來譔寫。
第三種方法當然也OK,但其實跟第一種方法沒什麼兩樣,甚至還會增加引入時的複雜度和錯誤的可能性。

4. 結論:
template算是泛型程式的基礎,讓程式碼可以依據使用者的需求,編譯出不同的執行檔。 也因此,在使用template時,程式的宣告和定義無法分離於不同的檔案。
一般來說,採用將宣告與定義放在同一個檔案內是基本的做法,可以保有未來的擴增性;如果這份程式只是自己的project內要使用,確定只支援哪些類型,就可以分離兩部分,並明確指明編譯哪些類型。

5. 致謝: 本文內容感謝黃偉寧(AZ Huang)同學指點,感謝諸位的觀看

2 則留言:

  1. C++ 11 好像有方法可以預先編譯需要的 template,可以減低編譯時間並避免重複的物件

    回覆刪除
  2. http://zh.wikipedia.org/wiki/C%2B%2B11#.E5.A4.96.E9.83.A8.E6.A8.A1.E6.9D.BF あったあった~

    回覆刪除