模板簡介
模板是C++一個非常重要的特性,它是C++泛型編程的基礎。某些對C++持極度偏見的人甚至說模板是C++對這個世界的
唯一貢獻(當然,我是不贊同的),可見模板在C++中的重要性,而整個STL都是基於模板的,可見其應用之廣泛。
C++引入模板的一個重要原因是算法的重用,比如下面一個例子:
[cpp]
bool mless_than(const int& v1, const int& v2) {
return v1 < v2;
}
程序很簡單,就是比較第一個參數是否小於第二個參數而已,這個算法在我們的程序可以說是非常常見,這個是int
版本的,如果我們還需要一個string版本的,一個double版本的,甚至是一個自定義類版本的怎麼辦呢?如果沒有模
板,我們就不得不為其定義多個實現,即使他們的代碼都是一樣的,如果算法很長,例如一個排序算法,寫這麼多個
版本將是一件冗長而乏味的事情,況且我們是無法預見未來需要為什麼樣的類型定制算法的,寫算法庫的人也無法知
道使用者可能會定義什麼樣的類型。說了模板的必要性,現在來看一個它的實現吧,還是剛才那個函數:
[cpp]
template<typename T>
bool mless_than(const T& v1, const T& v2) {
return v1 < v2;
}
使用的方式也很簡單,直接調用就是了,聰明的編譯器會自動為我們推導出模板的參數類型:
[cpp] view plaincopy
bool result = mless_than(2.8, 4.1);//double version
在某些情況下,編譯無法從調用參數推到出所有的模板類型,或是我們傳入的參數類型不是我們希望用於實例化函數
的模板參數類型時,我們也可以手動的指定模板參數類型,調用方式如下:
[cpp]
bool result = mless_than<int> (2.8, 4.1);//double version
模板除了用在函數上,還可以用在類中,例如:
[cpp]
template<typename T>
class A{
//...other definition
private:
T v;
//...other definition
}
如果經常使用STL的話,使用方式我們應該已經習慣了:
[cpp]
A<int> a;
這個是用一個int版本的類A去定義了一個對象a.
需要注意的是,無論是函數模板,還是類模板,它們都不是真正的函數或是類,它只是告訴編譯器該如何生成真正的
函數實例或是類實例(這裡並非對象哦),也就是說A並不是類,A<int>才是類。
關於模板的簡介就說到這吧,有了這個初步概念後,我們來看看class和typename的區別。
class和typename
在上面的模板定義中,我都是使用typename關鍵字來定義模板參數的,以前學習過或了解過模板的同學可能還會發現
另一個關鍵字class被重用在這裡用於定義模板參數,那麼它們究竟有上面區別呢?答案是在定義模板參數這裡,它
們是沒有區別的,由於typename是後引進的關鍵字,所以,在一些比較舊的代碼中,class關鍵可能會更加常見些。
我說了它們在這裡是沒有區別的,但既然我單獨列了一個小標題,說明這中間還是有點內容滴。typename在模板中還
有一些別的作用。在談這個之前,我們先來了解一個概念:nested dependent type names(嵌套依賴類型名)。考
慮下面的定義:
[cpp]
template<typename T>
void test(const T& c) {
c::key_type *ptr;
//other implementation
}
其中,c::key_type *ptr代表什麼意思呢?如果你對STL比較熟悉的話,你可能會說,嘿,這是利用c::key_type定義
了一個指針ptr,在map和set裡面都有這個類型,它表示的是其封裝的鍵的類型。可是,只是可是,如果有哪個傻瓜
自定義了一個類,並且他恰好在這個類內部又定義了一個名叫key_type的靜態變量,那麼這句話就不再是變量定義了
,而是一個乘法運算。那麼編譯器是如何看待這條語句的呢?首先還是說一下潛逃依賴類型名這個概念,像這種定義
在類內部的類型,而外部類的類型又是依賴於模板參數的,就是所謂的潛逃依賴類型名,默認情況下,編譯器是把它
當做是變量名,而非類型名的,也就是說默認下,編譯器會將其當做乘法運算的。如果我們想讓編譯器把它當做是類
型名,可以在其前面加上typename關鍵字進行修飾。
[cpp]
typename c::key_type *ptr;
對於像這種潛逃依賴類型名,我們在使用的時候都應該加上typename關鍵字進行修飾。但是,還是有例外的,例如在
基類列表裡面,如果有用到嵌套依賴類型,則不用typename關鍵字,因為這裡出現的標示符只可能是類型名。
Nontype 模板
前面所談及的都是類型模板,實際上C++的模板機制還支持非類型模板。為了更直觀的說明它是什麼,我們先來看一
個它在STL中一個實際運用的例子,那就是位圖類:
[cpp]
bitset<32> b;
如上的定義方式是定義了一個大小為32的位圖類,它利用非類型參數去指定其大小。我們來看一個非類型模板的簡單
實現:
[cpp]
template<int SIZE>
class A{
//some definition
int data[SIZE];
}
可以看出,非類型模板參數在這裡的作用是指定A<SIZE>內部維護的一個數組變量的大小。在我們進行參數傳遞時,
非類型參數有時候會先得非常有用,例如:
[cpp]
template <class T, size_t N> void array_init(T (&parm)[N])
{
for (size_t i = 0; i != N; ++i) {
parm[i] = 0;
}
}
該函數的作用是對任意大小的數組進行初始化,這裡巧妙的利用非類型參數指定了所傳遞的數組引用的大小。
模板的特化與偏特化
模板的作用是為任意的類型提供統一的實現方式,或是算法邏輯,或是數據結構。但有時候,對於某些類型,統一的
實現方式並不能滿足我們的要求,考慮最開始的那個模板函數,如果我們傳遞的是常量字符串,編譯器會為我們實例
化出這樣的函數代碼:
[cpp]
bool mless_than(const char* const& v1, const char* const& v2) {
return v1 < v2;
}
編譯運行都木有問題,可關鍵是,它比較的兩個字符串的地址,而非字符串本身,這顯然不是我們想要的。在這種情
況下,我們就需要利用模板的特化功能為它定制一個專門的版本用於處理C_style的字符串,特化的實現方式如下:
[cpp]
template<>
bool mless_than(const char* const& v1, const char* const& v2) {
return strcmp(v1, v2) < 0;
}
當我們傳遞C_style的字符串時,編譯器就會調用我們特化的這個版本,而不是利用模板去給我們呢生成。
模板的特化也可以用在類模板上,在特化的類中,我們不必遵循原先模板的定義方式。類模板的特化定義方式和函數
模板大致類似,不過,它必須在類名後面顯示的指定特化的模板參數類型。
除了模板特化,C++還允許我們對類模板進行偏特化(函數模板不行),就是只針對部分模板參數進行特化,例如:
[cpp]
template<typename T, typename V>
class A{
//other definition
T d1;
V d2;
};
template<typename T>
class A<T, int>{
//other definition
T d1;
int d2;
};
在偏特化中,我們將模板參數V特化為int,需要注意的是,偏特化後的模板仍然是一個模板類,而非實際的類。
模板元編程
所謂模板元編程實際上並沒有引入新的C++特性,它是C++非類型模板與模板特化的一個非常奇妙的用法。它能夠將一
些運行時計算的任務放到編譯期來完成,從而提高運行效率。例如,我們希望以常量的階乘作為一個靜態數組的大小
,就可以利用模板元編程了:
[cpp]
template<unsigned N>
class Factorial {
public:
unsigned VALUE = N*factorial<N-1>::VALUE;
};
template<>
class Factorial<0> {
public:
unsigned VALUE = 1;
};
上面的模板類Factorial用於計算階乘,它巧妙的利用遞歸在編譯器就可以計算出我們所需要的階乘值,值得注意的
是,這裡的遞歸出口是一個偏特化模板,很神奇吧。
模板的編譯機制
談完模板的一些基本特性與使用方式,我們最後來看一下模板編譯機制。我們知道,模板只是提供被編譯器供編譯器
生成實例的一種方式。而模板是按需進行實例化的,也就是說,如果我們按照我們的習慣將類模板的定義放在頭文件
裡,而將其成員函數的實現放在源文件中,在對定義源文件進行編譯時是不會生成任何代碼的,這樣在其他使用文件
中因為只包含器類的定義而不包含實現,在連接過程中就會發生找不到類的錯誤。所以我們的模板代碼必須全部放在
頭文件中,如果你為了方便管理,也可以講實現放在源文件中,再通過頭文件進行反包含。有些童鞋可能會擔心重定
義的問題,實際上把模板放在頭文件中,這種C++標准中是運行的,編譯器對它做了特殊處理,所以不會有普特類的
定義放在頭文件中的擔心。
實際上在C++的標准中還有一個關鍵字export可以解決這個問題,不過由於目前還沒多少編譯器支持,所以這裡也就
不談了。
模板的編譯機制是按需的,不僅對於類型按需,對於那些並沒有使用到的成員函數,編譯器也不會對它進行編譯的,
這點很重要。前段時間,在網上翻博客時就碰見一位仁兄講述的一個例子,他自己做了一個簡單的模板庫,在測試的
時候沒有問題,但在實際的使用過程中卻碰到了大麻煩。因為他在測試時,對於有些類型並沒有用到所有的成員函數
,而這些成員函數編譯器就沒有對它進行編譯,這才使問題在使用期才暴露出來。所以,了解模板的編譯機制,對於
我們以後開發自己的模板庫有很重要的作用。
作者:justaipanda