模板（Template）在 C++ 程式設計語言中，是指函式模板與類模板，是一種參數化類型機制。Java 和 C＃中的泛型與 C++ 的模板大體對應，但也有一些功能上的顯著差異（C++ 模板支援兩者沒有明確對應的模板模板參數和模板非類型參數，但不支援 Java 的萬用字元以及 C# 的泛型類型約束）。模板是 C++ 的泛型編程中不可缺少的一部分。

模板是 C++ 程式設計師絕佳的武器，特別是結合了多重繼承與運算子多載之後。C++ 的標準函式庫提供的許多有用的函式大多結合了模板的概念，如 STL 以及 iostream。



語法

模板的聲明與定義

模板定義以關鍵字 `template` 開始，後接模板形參表（template parameter list），模板形參表是用尖括號括住的一個或者多個模板形參的列表，形參之間以逗號分隔。模板形參可以是表示類型的類型形參（type parameter），也可以是表示常數表達式的非類型形參（non-type parameter）。非類型形參跟在類型說明符之後聲明。類型形參跟在關鍵字 class 或 typename 之後聲明。模板形參可以給出預設值（default arguments for template parameters）。

模板的非類型形參

模板的非類型形參（template non-type parameter）允許為下述形式：


* 整型或列舉型

* 到物件的指標或函式指標

* 到物件的參照或函式參照

* 成員指標

模板的非類型參數被聲明為陣列或函式的，將被轉換為指標或函式指標。例如：

```
 template<int a[4]> struct A { };
 template<int f(int)> struct B { };
 int i;
 int g(int) { return 0;}
 A<&i> x;
 B<&g> y;
```

模板的非類型形參允許用 const 或 volatile 限定（而模板的類型形參是不允許 cv 限定的）。模板的非類型形參是不允許聲明為浮點型、class 類型、void 型。

模板的模板參數

類別模板的模板參數允許是另外一個類別模板，這稱為模板的模板參數（template template parameter），也譯作「模板參數模板」。函式模板不允許有模板的模板參數。例如：

```
template<template <class T> class X> class A { }; //类模板A的第二个参数是另外一个类模板X
template<class T> class B { };
A<B> a; //模板A的实际使用。其中的B是模板的模板实参（template template argument）
```

模板參數的預設值

模板形參可以給出預設值（default arguments for template parameters）。如果一個模板參數給出了預設值，那麼模板形參列表中在其後聲明的模板參數都應該給出預設值。例如：

```
template<class T = char, class U, class V = int> class X { }; //编译出错，或者给出U的默认值，或者不给出T的默认值
```

一個模板的各次聲明給出的模板參數的預設值可以累積其效果。例如：

```
template<class T, class U = int> class A;
template<class T = float, class U> class A;
template<class T, class U> class A {
  public:
     T x;
     U y;
};
A<> a; //a.x is float, and the type of a.y is int
```

但是如果交換本範例第一行與第二行的次序，將編譯報錯。因為如果第一個模板參數 T 有了預設值，此時編譯器必須已經知道其後的第二個模板參數 U 的預設值。

在同一個作用域（scope）中，不能對同一個模板的同一個參數多次聲明其預設值。例如：

```
template<class T = char> class X;
template<class T = char> class X { };//编译报错。如果在本行中不给出模板参数T的默认值将编译通过
```

模板參數的作用域為從其聲明之處至該模板的定義結束之處。因此可以使用一個模板參數作為其後聲明的其他模板參數的一部分或預設值。例如：

```
template<class V, V obj> class C; 
template<class T, class U = T> class D { };
```

變數模板

變數模板（variable template）是 C++14 引入的新的一個種類的模板。可用於在命名空間作用域聲明一個變數。例如：

```
template<class T>
constexpr T pi = T(3.1415926535897932385);  // variable template
template<class T>
T circular_area(T r) // function template
{
   return pi<T> 
* r 
* r; // pi<T> is a variable template instantiation
}
```

可以在類作用域聲明一個靜態資料成員：

```
struct matrix_constants
{
   template<class T>
   using pauli = hermitian_matrix<T, 2>; // alias template
   template<class T> static constexpr pauli<T> sigma1 = { { 0, 1 }, { 1, 0 } }; // static data member template
   template<class T> static constexpr pauli<T> sigma2 = { { 0, -1i }, { 1i, 0 } };
   template<class T> static constexpr pauli<T> sigma3 = { { 1, 0 }, { 0, -1 } };
};
```

類的靜態資料成員模板，也可以用類別模板的非模板資料成員來實現：

```
struct limits {
   template<typename T>
   static const T min; // declaration of a static data member template
};
template<typename T> const T limits::min = { }; // definition of a static data member template
template<class T> class X {
    static T s; // declaration of a non-template static data member of a class template
};
template<class T> T X<T>::s = 0; // definition of a non-template data member of a class template

```

變數模板不能用作模板的模板參數（template template arguments）。

模板的使用

使用模板時，可以在模板名字後面顯式給出用尖括號括住的模板實參列表（template argument list）。對模板函式或類的模板成員函式，也可不顯式給出模板實參，而是由編譯器根據函式呼叫的上下文推導出模板實參，這稱為模板參數推導。

如果模板參數使用其預設值，則在模板實參列表中可以忽略它。如果所有的模板參數都使用了預設值，模板實參列表為空，但仍然必須寫出成對的尖括號。例如：

```
template<class T = int> class X { };
X<> a; //编译通过
X b;   //编译报错
```

對於作為類型的模板實參，不允許是局部類型（local type）、無連結性的類型（type with no linkage）、無名類型（unnamed type）或包括了這三種情形的複合類型。但 C++11 以及允許本地類型作為模板實參。



範例

函式模板

以下以取最大值的函式模板 maximum 為例。此函式在編譯時會自動產生對應參數類型的代碼，而不用顯式聲明。

類別模板

以下以將元件指標的操作，封裝成類別模板 ComPtr 為例。

模板的巢狀：成員模板

對於類中的模板成員函式、巢狀的成員類別模板，可以在封閉類的內部或外部定義它們。當模板成員函式、巢狀類別模板在其封閉類的外部定義時，必須以封閉類別模板的模板參數（如果它們也是模板類）和成員模板的模板參數開頭。如下例：

C++ 標準規定：如果外圍的類別模板沒有特例化，裡面的成員模板就不能特例化。例如：



依賴名字與 typename 關鍵字

一個模板中的依賴於一個模板參數（template parameter）的名字被稱為依賴名字 （dependent name）。當一個依賴名字巢狀在一個類的內部時，稱為巢狀依賴名字（nested dependent name）。一個不依賴於任何模板參數的名字，稱為非依賴名字（non-dependent name）。

編譯器在處理模板定義時，可能並不確定依賴名字表示一個類型，還是巢狀類的成員，還是類的靜態成員。C++ 標準規定：如果解析器在一個模板中遇到一個巢狀依賴名字，它假定那個名字不是一個類型，除非顯式用 typename 關鍵字前置修飾該名字。

typename 關鍵字有兩個用途：

1. 常見的在模板定義中的模板形參列表，表示一個模板參數是類型參數。等同於使用 class。
2. 使用模板類內定義的巢狀依值型別名字時，顯式指明這個名字是一個類型名。否則，這個名字會被理解為模板類的靜態成員名。C++11 起，這一用途也可以出現在模板以外，儘管此時 `typename` 關鍵字不是必要的。

在下述情形，對巢狀依值型別名字不需要前置修飾 typename 關鍵字：


* 衍生類別聲明的基礎類別列表中的基礎類別識別碼；

* 成員初始化列表中的基礎類別識別碼；

* 用 `class`、`struct`、`enum` 等關鍵字開始的類型識別碼

因為它們的上下文已經指出這些識別碼就是作為類型的名字。例如：



template 關鍵字

template 關鍵字有兩個用途：

1. 常見的在模板定義的開始。
2. 模板類內部定義了模板成員函式或者巢狀的成員模板類。在模板中，當參照這樣的模板成員函式或巢狀的成員模板類時，可以在 `::`（作用域解析）運算子、`.`（以物件方式存取成員）運算子、`->`（以指標方式存取成員）運算子之後使用 template 關鍵字，隨後才是模板成員函式名字或巢狀的成員模板類名字，這使得隨後的左尖括號 `<` 被解釋為模板參數列的開始，而不是小於號運算子。C++11 起，這一用途也可以出現在模板以外，儘管此時 `template` 關鍵字不是必要的。例如：



別名模板

別名模板（aliase template）是 C++11 引入的技術。在 C++03 標準中，可以用 typedef 給全特化模板定義新的類型名。但是不允許用 typedef 施加於偏特化模板上。例如：

C++11 增加了給偏特化模板增加別名的功能，例如：

`using` 在 C++11 中也可用於其他的類型別名的聲明:



模板實例化

模板實例化（template instantiation）是指在編譯或連結時生成函式模板或類別模板的具體實例原始碼。ISO C++ 定義了兩種模板實例化方法：隱式實例化（當使用實例化的模板時自動地在當前代碼單元之前插入模板的實例化代碼）、顯式實例化（直接聲明模板實例化）。在 C++ 語言的不同實現中，模板編譯模式（模板初始化的方法）大致可分為三種：


* Borland 模型（包含模板編譯模式）：編譯器生成每個編譯單元中遇到的所有的模板實例，並存放在相應的目的檔中；連結器合併相同的模板實例，生成可執行檔。為了在每次模板實例化時模板的定義都是可見的，模板的聲明與定義放在同一個.h 檔案中。這種方法的優點是連結器只需要處理目的檔；這種方法的缺點是由於模板實例被重複編譯，編譯時間被加長了，而且不能使用系統的連結器，需重新設計連結器。

* Cfront / 查詢模型（分離（Separation）模板編譯模式）：AT&T 公司的 C++ 編譯器 Cfront 為解決模板實例化問題，增加了一個模板倉庫，用以存放模板實例的代碼並可被自動維護。當生成一個目的檔時，編譯器把遇到的模板定義與當前可生成的模板實例存放到模板倉庫中。連結時，連結器的包裝程式（wrapper）首先呼叫編譯器生成所有需要的且不在模板倉庫中的模板實例。這種方法的優點是編譯速度得到了最佳化，而且可以直接使用系統的連結器；這種方法的缺點是複雜度大大增加，更容易出錯。使用這種模型的源程式通常把模板聲明與非行內的模板成員分別放在.h 檔案與模板定義檔案中，後者單獨編譯。

* 混合（迭代）模型：g++ 目前是基於 Borland 模型完成模板實例化。g++ 未來將實現混合模型的模板實例化，即編譯器把編譯單元中的模板定義與遇到的當前可實現的模板實例存放在相應的目的檔中；連結器的包裝程式（wrapper）呼叫編譯器生成所需的目前還沒有實例化的模板實例；連結器合併所有相同的模板實例。使用這種模型的源程式通常把模板聲明與非行內的模板成員分別放在.h 檔案與模板定義檔案中，後者單獨編譯。

ISO C++ 標準規定，如果隱式實例化模板，則模板的成員函式一直到參照時才被實例化；如果顯式實例化模板，則模板所有成員立即都被實例化，所以模板的聲明與定義在此處都應該是可見的，而且在其它程式文字檔案使用了這個模板實例時用編譯器選項抑制模板隱式實例化，或者模板的定義部分是不可見的，或者使用 _template<> type FUN\_NAME(type list)_ 的語句聲明模板的特化但不實例化。

g++ 的模板實例化，目前分為三種方式：


* 不指定任何特殊的編譯器參數：按 Borland 模型寫的原始碼能正常完成模板實例化，但每個編譯單元將包含所有它用到的模板實例，導致在大的程式中無法接受的代碼冗餘。需要用 GNU 的連結器刪除各個目的檔中冗餘的模板實例，不能使用作業系統提供的連結器。

* 使用-fno-implicit-templates 編譯選項：在生成目的檔時完全禁止隱式的模板實例化，所有模板實例都顯式的寫出來，可以存放在一個單獨的原始檔中；也可以存放在各個模板定義檔案中。如果一個很大的原始檔中使用了各個模板實例，這個原始檔不用 - fno-implicit-templates 選項編譯，就可以自動隱式的生成所需要的模板實例。在生成庫檔案時這個編譯選項特別有用。

* 使用-frepo 編譯選項：在生成每個目的檔時，把需要用到的當前可生成的模板實例存放在相應的.rpo 檔案中。連結器包裝程式 (wrapper)—collect2 將刪除.rpo 檔案中冗餘的模板實例並且修改相應的.rpo 檔案，使得編譯器可以利用.rpo 檔案知道在那裡正確放置、參照模板實例，並重新編譯生成受影響的目的檔。由作業系統的通用的連結器生成可執行檔。這對 Borland 模型是很好的模板實例化方法。對於使用 Cfront 模型的軟體，需要修改原始碼，在模板標頭檔的末尾加上_#include <tmethods.cc>_。不過 MinGW 中不包含連結器包裝程式 collect2，故不使用此方法。對於庫 (library)，建議使用顯式實例化方法。

* 另外，g++ 擴充了 ISO C++ 標準，用 extern 關鍵字指出模板實例在其它編譯單元中顯式聲明（這已經被 C++11 標準接受）；用 inline 關鍵字實例化編譯器支援的資料（如類的虛表）但不實例化模板成員；用 static 關鍵字實例化模板的靜態資料成員但不實例化其它非靜態的模板成員。

* g++ 不支援模板實例化的 export 關鍵字（此關鍵字的這個用法已在 C++11 標準里被取消）。

VC++7.0 中必須類別模板實例化只有 Borland 模型；函式模板一般隱式實例化，自 5.0 版以後也可顯式實例化。



參考文獻