模板(Templates)是ANSI-C++ 標準中新引入的概念。如果你使用的 C++ 編譯器不符合這個標準,則你很可能不能使用模板。
函數模板( Function templates)
模板(Templates)使得我們可以產生通用的函數,這些函數能夠接受任意資料類型的參數,可返回任意類型的值,而不需要對所有可能的資料類型進行函數重載。這在一定程度上實現了宏(macro)的作用。它們的原型定義可以是下面兩種中的任何一個:
template <class identifier> function_declaration;
template <typename identifier> function_declaration;
上面兩種原型定義的不同之處在關鍵字class 或 typename的使用。它們實際是完全等價的,因為兩種表達的意思和執行都一模一樣。
例如,要產生一個模板,返回兩個對象中較大的一個,我們可以這樣寫:
template <class GenericType>
GenericType GetMax (GenericType a, GenericType b) { return (a>b?a:b); }
在第一行聲明中,我們已經產生了一個通用資料類型的模板,叫做GenericType。因此在其後面的函數中,GenericType 成為一個有效資料類型,它被用來定義了兩個參數a和 b ,並被用作了函數GetMax的傳回值類型。
GenericType 仍沒有代表任何具體的資料類型;當函數 GetMax 被調用的時候,我們可以使用任何有效資料類型來調用它。這個資料類型將被作為pattern來代替函數中GenericType 出現的地方。用一個類型pattern來調用一個模板的方法如下:
function <type> (parameters);
例如,要調用GetMax 來比較兩個int類型的整數可以這樣寫:
int x,y;
GetMax <int> (x,y);
因此,GetMax 的調用就好像所有的GenericType 出現的地方都用int 來代替一樣。
這裡是一個例子:
// function template #include <iostream.h> template <class T> T GetMax (T a, T b) { T result; result = (a>b)? a : b; return (result); } int main () { int i=5, j=6, k; long l=10, m=5, n; k=GetMax(i,j); n=GetMax(l,m); cout << k << endl; cout << n << endl; return 0; } |
6 10 |
(在這個例子中,我們將通用資料類型命名為T 而不是 GenericType ,因為T短一些,並且它是模板更為通用的標示之一,雖然使用任何有效標示符都是可以的。)
在上面的例子中,我們對同樣的函數GetMax()使用了兩種參數類型:int 和 long,而唯寫了一種函數的實現,也就是說我們寫了一個函數的模板,用了兩種不同的pattern來調用它。
如你所見,在我們的模板函數 GetMax() 裡,類型 T 可以被用來聲明新的對象
T result;
result 是一個T類型的對象, 就像a 和 b一樣,也就是說,它們都是同一類型的,這種類型就是當我們調用模板函數時寫在角括弧<> 中的類型。
在這個具體的例子中,通用類型 T 被用作函數GetMax 的參數,不需要說明<int>或 <long>,編譯器也可以自動檢測到傳入的資料類型,因此,我們也可以這樣寫這個例子:
int i,j;
GetMax (i,j);
因為i 和j 都是int 類型,編譯器會自動假設我們想要函數按照int進行調用。這種暗示的方法更為有用,併產生同樣的結果:
// function template II #include <iostream.h> template <class T> T GetMax (T a, T b) { return (a>b?a:b); } int main () { int i=5, j=6, k; long l=10, m=5, n; k=GetMax(i,j); n=GetMax(l,m); cout << k << endl; cout << n << endl; return 0; } |
6 10 |
注意在這個例子的main() 中我們如何調用模板函數GetMax() 而沒有在括弧<>中指明具體資料類型的。編譯器自動決定每一個調用需要什麼資料類型。
因為我們的模板函數只包括一種資料類型 (class T), 而且它的兩個參數都是同一種類型,我們不能夠用兩個不同類型的參數來調用它: int i;
long l;
k = GetMax (i,l);
上面的調用就是不對的,因為我們的函數等待的是兩個同種類型的參數。
我們也可以使得模板函數接受兩種或兩種以上類型的資料,例如:
template <class T>
T GetMin (T a, U b) { return (a<b?a:b); }
在這個例子中,我們的模板函數 GetMin() 接受兩個不同類型的參數,並返回一個與第一個參數同類型的對象。在這種定義下,我們可以這樣調用該函數: int i,j;
long l;
i = GetMin <int, long> (j,l);
或者,簡單的用
i = GetMin (j,l);
雖然 j 和 l 是不同的類型。
類模板(Class templates)
我們也可以定義類模板(class templates),使得一個類可以有基於通用類型的成員,而不需要在類產生的時候定義具體的資料類型,例如:
template <class T>
class pair {
T values [2];
public:
pair (T first, T second) {
values[0]=first;
values[1]=second;
}
};
上面我們定義的類可以用來儲存兩個任意類型的元素。例如,如果我們想要定義該類的一個對象,用來儲存兩個整型資料115 和 36 ,我們可以這樣寫:
pair<int> myobject (115, 36);
我們同時可以用這個類來產生另一個對象用來儲存任何其他類型資料,例如:
pair<float> myfloats (3.0, 2.18);
在上面的例子中,類的唯一一個成員函數已經被inline 定義。如果我們要在類之外定義它的一個成員函數,我們必須在每一函數前面加template <... >。
// class templates #include <iostream.h> template <class T> class pair { T value1, value2; public: pair (T first, T second) { value1=first; value2=second; } T getmax (); };
template <class T> T pair::getmax (){ T retval; retval = value1>value2? value1 : value2; return retval; }
int main () { pair myobject (100, 75); cout << myobject.getmax(); return 0; } |
100 |
注意成員函數getmax 是怎樣開始定義的:
template <class T>
T pair::getmax ()
所有寫 T 的地方都是必需的,每次你定義模板類的成員函數的時候都需要遵循類似的格式(這裡第二個T表示函數傳回值的類型,這個根據需要可能會有變化)。
模板特殊化(Template specialization)
模板的特殊化是當模板中的pattern有確定的類型時,模板有一個具體的實現。例如假設我們的類模板pair 包含一個模數計算(module operation)的函數,而我們希望這個函數只有當對象中儲存的資料為整型(int)的時候才能工作,其他時候,我們需要這個函數總是返回0。這可以通過下面的代碼來實現:
// Template specialization #include <iostream.h> template <class T> class pair { T value1, value2; public: pair (T first, T second){ value1=first; value2=second; } T module () {return 0;} }; template <> class pair <int> { int value1, value2; public: pair (int first, int second){ value1=first; value2=second; } int module (); };
template <> int pair<int>::module() { return value1%value2; }
int main () { pair <int> myints (100,75); pair <float> myfloats (100.0,75.0); cout << myints.module() << '\n'; cout << myfloats.module() << '\n'; return 0; } |
25 0 |
由上面的代碼可以看到,模板特殊化由以下格式定義:
template <> class class_name <type>
這個特殊化本身也是模板定義的一部分,因此,我們必須在該定義開頭寫template <>。而且因為它確實為一個具體類型的特殊定義,通用資料類型在這裡不能夠使用,所以第一對角括弧<> 內必須為空白。在類名稱後面,我們必須將這個特殊化中使用的具體資料類型寫在角括弧<>中。
當我們特殊化模板的一個資料類型的時候,同時還必須重新定義類的所有成員的特殊化實現(如果你仔細看上面的例子,會發現我們不得不在特殊化的定義中包含它自己的建構函式 constructor,雖然它與通用模板中的建構函式是一樣的)。這樣做的原因就是特殊化不會繼承通用模板的任何一個成員。
模板的參數值(Parameter values for templates)
除了模板參數前面跟關鍵字class 或 typename 表示一個通用類型外,函數模板和類模板還可以包含其它不是代表一個類型的參數,例如代表一個常數,這些通常是基礎資料型別 (Elementary Data Type)的。例如,下面的例子定義了一個用來儲存數組的類模板:
// array template #include <iostream.h> template <class T, int N> class array { T memblock [N]; public: void setmember (int x, T value); T getmember (int x); }; template <class T, int N> void array<T,N>::setmember (int x, T value) { memblock[x]=value; } template <class T, int N> T array<T,N>::getmember (int x) { return memblock[x]; } int main () { array <int,5> myints; array <float,5> myfloats; myints.setmember (0,100); myfloats.setmember (3,3.1416); cout << myints.getmember(0) << '\n'; cout << myfloats.getmember(3) << '\n'; return 0; } |
100 3.1416 |
我們也可以為模板參數設定預設值,就像為函數參數設定預設值一樣。
下面是一些模板定義的例子:
template <class T> // 最常用的:一個class 參數。
template <class T, class U> // 兩個class 參數。
template <class T, int N> // 一個class 和一個整數。
template <class T = char> // 有一個預設值。
template <int Tfunc (int)> // 參數為一個函數。
模板與多檔案工程 (Templates and multiple-file projects)
從編譯器的角度來看,模板不同於一般的函數或類。它們在需要時才被編譯(compiled on demand),也就是說一個模板的代碼直到需要產生一個對象的時候(instantiation)才被編譯。當需要instantiation的時候,編譯器根據模板為特定的調用資料類型產生一個特殊的函數。
當工程變得越來越大的時候,程式碼通常會被分割為多個來源程式檔案。在這種情況下,通常介面(interface)和實現(implementation)是分開的。用一個函數庫做例子,介面通常包括所有能被調用的函數的原型定義。它們通常被定義在以.h 為副檔名的標頭檔 (header file) 中;而實現 (函數的定義) 則在獨立的C++代碼檔案中。
模板這種類似宏(macro-like) 的功能,對多檔案工程有一定的限制:函數或類模板的實現 (定義) 必須與原型聲明在同一個檔案中。也就是說我們不能再 將介面(interface)儲存在單獨的標頭檔中,而必須將介面和實現放在使用模板的同一個檔案中。
回到函數庫的例子,如果我們想要建立一個函數模板的庫,我們不能再使用標頭檔(.h) ,取而代之,我們應該產生一個模板檔案(template file),將函數模板的介面和實現 都放在這個檔案中 (這種檔案沒有慣用副檔名,除了不要使用.h副檔名或不要不加任何副檔名)。在一個工程中多次包含同時具有聲明和實現的模板檔案並不會產生連結錯誤 (linkage errors),因為它們只有在需要時才被編譯,而相容模板的編譯器應該已經考慮到這種情況,不會產生重複的代碼。