模板 (C++)

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

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

語法

1.1 模板的宣告與定義

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

1.1.1 模板的非型別形參

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

整型或列舉型
到物件的指標或函式指標
到物件的引用或函式引用
成員指標

模板的非型別引數被宣告為陣列或函式的，將被轉換為指標或函式指標。例如：

template struct A { };

template struct B { };

int i;

int g (int) {return 0;}

A x;

B y;

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

1.1.2 模板的模板引數

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

template class X> class A { }; // 類模板 A 的第二個引數是另外一個類模板 X

template class B { };

A a; // 模板 A 的實際使用。其中的 B 是模板的模板實參（template template argument）

1.1.3 模板引數的預設值

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

template class X { }; // 編譯出錯，或者給出 U 的預設值，或者不給出 T 的預設值

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

template class A;

template class A;

template 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 X;

template class X { };// 編譯報錯。如果在本行中不給出模板引數 T 的預設值將編譯透過

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

template class C;

template class D { };

1.1.4 變數模板

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

template

constexpr T pi = T (3.1415926535897932385); //variable template

template

T circular_area (T r) //function template

{return pi r r; //pi is a variable template instantiation}

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

struct matrix_constants

{

template

using pauli = hermitian_matrix; //alias template

template static constexpr pauli sigma1 = {{ 0, 1}, {1, 0} }; //static data member template

template static constexpr pauli sigma2 = {{ 0, -1i}, {1i, 0} };

template static constexpr pauli sigma3 = {{ 1, 0}, {0, -1} };

};

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

struct limits {

template

static const T min; //declaration of a static data member template

};

template const T limits::min = { }; //definition of a static data member template

template class X {static T s; //declaration of a non-template static data member of a class template};

template T X::s = 0; //definition of a non-template data member of a class template

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

1.2 模板的使用

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

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

template class X { };

X a; // 編譯透過

X b; // 編譯報錯

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

示例

2.1 函式模板

以下以取最大值的函式模板 maximum 為例。此函式在編譯時會自動產生對應引數型別的程式碼，而不用顯式宣告。

include

template

inline const T& maximum (const T& x,const T& y)

{if (y> x){return y;}

else {return x;}

}

int main (void)

{

using namespace std;

int a=3,b=7;

float x=3.0,y=7.0;

//Calling template function

std::cout (a,b) (x,y)

2.2 類模板

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

pragma once

template

class ComPtr

{

protected:

Ty* m_ptr;

public:

ComPtr

{m_ptr = NULL;}

ComPtr (const ComPtr& rhs)

{

m_ptr = NULL;

SetComPtr (rhs.m_ptr);

}

ComPtr (Ty* p)

{

m_ptr = NULL;

SetComPtr (p);

}

~ComPtr

{Release ;

}

const ComPtr& operator=(const ComPtr& rhs)

{SetComPtr (rhs.m_ptr);

return *this;

}

Ty operator=(Ty p)

{SetComPtr (p);

return p;

}

operator Ty*

{return m_ptr;}

Ty* operator->

{return m_ptr;}

operator Ty

{Release ;

return &m_ptr;

}

operator void

{Release ;

return (void)&m_ptr;

}

bool IsEmpty

{return (m_ptr == NULL);

}

void SetComPtr (Ty* p)

{Release ;

m_ptr = p;

if (m_ptr)

{m_ptr->AddRef ;}

}

void Release

{if (m_ptr)

{m_ptr->Release ;

m_ptr = NULL;

}

}

};

2.3 模板的巢狀：成員模板

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

template class myc {

public:

template C foo (S s);

};

// 下行需要給出外部類與內部巢狀類的模板形參列表：

template template C myc::foo (S s){

C var;

return var;

}

int main

{

float f;

myc v1;

v1.foo (f);

}

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

template class A {

template class B {template void mf1 (T3);

void mf2 ;};

};

template template

class A::B {template void mf1 (T);

};

template template template

void A::B::mf1 (T t) { }

template template

void A::B::mf2 //ill-formed; B is specialized but its enclosing class template A is not

依賴名字與 typename 關鍵字

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

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

typename 關鍵字有兩個用途：

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

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

派生類宣告的基類列表中的基類識別符號；
成員初始化列表中的基類識別符號；
用 class、struct、enum 等關鍵字開始的型別識別符號

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

template class A: public T::Nested { // 基類列表中的 T::Nested

public:

A (int x) : T::Nested (x) ; // 成員初始化列表中的 T::Nested

struct T::type1 m; // 已經有了 struct 關鍵字的 T::type1

};

class B {

public:

class Nested {

public:

Nested (int x);};

typedef struct {int x;} type1;

};

int main

{A a (101);

return 0;

}

template 關鍵字

template 關鍵字有兩個用途：

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

class A { public:

template class B {public: typedef int INT;};

template void foo

};

template

int f

{

typename T::template B::INT i;

i=101;

T a, *p=&a;

a.template foo ;

p->template foo ;

return 0;

}

int main

{f ;

A::B::INT i; // 自 C++11 起，也可寫作 typename A::template B::INT i;

}

別名模板

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

template

class SomeType;

template

typedef SomeType TypedefName; // Illegal in C++03

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

template

class SomeType;

template

using TypedefName = SomeType;

using 在 C++11 中也可用於其他的類型別名的宣告:

typedef void (*FunctionType)(double); // Old style

using FunctionType1 = void (*)(double); // New introduced syntax

模板例項化

模板例項化（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 。不過 MinGW 中不包含連結器包裝程式 collect2，故不使用此方法。對於庫 (library)，建議使用顯式例項化方法。
另外，g++ 擴充套件了 ISO C++ 標準，用 extern 關鍵字指出模板例項在其它編譯單元中顯式宣告（這已經被 C++11 標準接受）；用 inline 關鍵字例項化編譯器支援的資料（如類的虛表）但不例項化模板成員；用 static 關鍵字例項化模板的靜態資料成員但不例項化其它非靜態的模板成員。
g++ 不支援模板例項化的 export 關鍵字（此關鍵字的這個用法已在 C++11 標準裡被取消）。

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