模板 (C++)
模板(Template)在C++程式語言中,是指函數模板與類模板[1],是一種參數化類型機制。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)或包括了這三種情形的複合類型。[2]但C++11以及允許本地類型作為模板實參。
範例
函數模板
以下以取最大值的函數模板maximum為例。此函數在編譯時會自動產生對應參數類型的代碼,而不用顯式聲明。
#include <iostream>
template <typename T>
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 << maximum<int>(a,b) << std::endl; //输出 7
std::cout << maximum(a, b) << std::endl; //自动补充类型声明
std::cout << maximum<double>(x,y) << std::endl; //输出 7
return 0;
}
類別模板
- 以下以將元件指標的操作,封裝成類別模板ComPtr為例。
#pragma once
template <typename Ty>
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;
}
}
};
模板的巢狀:成員模板
對於類中的模板成員函數、巢狀的成員類別模板,可以在封閉類的內部或外部定義它們。當模板成員函數、巢狀類別模板在其封閉類的外部定義時,必須以封閉類別模板的模板參數(如果它們也是模板類)和成員模板的模板參數開頭。[1]如下例:
template <typename C> class myc{
public:
template <typename S> C foo(S s);
};
//下行需要给出外部类与内部嵌套类的模板形参列表:
template<typename C> template <typename S> C myc<C>::foo(S s){
C var;
return var;
}
int main()
{
float f;
myc<int> v1;
v1.foo(f);
}
C++標準規定:如果外圍的類別模板沒有特例化,裏面的成員模板就不能特例化[3]。例如:
template <class T1> class A {
template<class T2> class B {
template<class T3> void mf1(T3);
void mf2();
};
};
template <> template <class X>
class A<int>::B {
template <class T> void mf1(T);
};
template <> template <> template<class T>
void A<int>::B<double>::mf1(T t) { }
template <class Y> template <>
void A<Y>::B<double>::mf2() { } // ill-formed; B<double> is specialized but its enclosing class template A is not
依賴名字與typename關鍵字
一個模板中的依賴於一個模板參數(template parameter)的名字被稱為依賴名字 (dependent name)。當一個依賴名字巢狀在一個類的內部時,稱為巢狀依賴名字(nested dependent name)。一個不依賴於任何模板參數的名字,稱為非依賴名字(non-dependent name)。[4]
編譯器在處理模板定義時,可能並不確定依賴名字表示一個類型,還是巢狀類的成員,還是類的靜態成員。C++標準規定:如果解析器在一個模板中遇到一個巢狀依賴名字,它假定那個名字不是一個類型,除非顯式用typename關鍵字前置修飾該名字。[5]
typename關鍵字有兩個用途:
- 常見的在模板定義中的模板形參列表,表示一個模板參數是類型參數。等同於使用class。
- 使用模板類內定義的巢狀依值型別名字時,顯式指明這個名字是一個類型名。否則,這個名字會被理解為模板類的靜態成員名。C++11起,這一用途也可以出現在模板以外,儘管此時
typename
關鍵字不是必要的。
在下述情形,對巢狀依值型別名字不需要前置修飾typename關鍵字:[6]
- 衍生類別聲明的基礎類別列表中的基礎類別識別碼;
- 成員初始化列表中的基礎類別識別碼;
- 用
class
、struct
、enum
等關鍵字開始的類型識別碼
因為它們的上下文已經指出這些識別碼就是作為類型的名字。例如:
template <class T> 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<B> a(101);
return 0;
}
template關鍵字
template關鍵字有兩個用途:
- 常見的在模板定義的開始。
- 模板類內部定義了模板成員函數或者巢狀的成員模板類。在模板中,當參照這樣的模板成員函數或巢狀的成員模板類時,可以在
::
(作用域解析)運算子、.
(以對象方式訪問成員)運算子、->
(以指標方式訪問成員)運算子之後使用template關鍵字,隨後才是模板成員函數名字或巢狀的成員模板類名字,這使得隨後的左尖括號<
被解釋為模板參數列的開始,而不是小於號運算子。C++11起,這一用途也可以出現在模板以外,儘管此時template
關鍵字不是必要的。例如:
class A { public:
template <class U> class B{
public: typedef int INT;
};
template <class V> void foo(){}
};
template <typename T>
int f()
{
typename T::template B<double>::INT i;
i=101;
T a, *p=&a;
a.template foo<char>();
p->template foo<long>();
return 0;
}
int main()
{
f<A>();
A::B<double>::INT i; // 自C++11起,也可写作typename A::template B<double>::INT i;
}
別名模板
別名模板(aliase template)是C++11引入的技術。在C++03標準中,可以用typedef給全特化模板定義新的類型名。但是不允許用typedef施加於偏特化模板上。例如:
template <typename First, typename Second, int Third>
class SomeType;
template <typename Second>
typedef SomeType<OtherType, Second, 5> TypedefName; // Illegal in C++03
C++11增加了給偏特化模板增加別名的功能,例如:
template <typename First, typename Second, int Third>
class SomeType;
template <typename Second>
using TypedefName = SomeType<OtherType, Second, 5>;
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)的陳述式聲明模板的特化但不實例化。
- 不指定任何特殊的編譯器參數:按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版以後也可顯式實例化。
參考文獻
- ^ 1.0 1.1 MSDN:嵌套的类模板. [2014-09-25]. (原始內容存檔於2016-05-29).
- ^ §14.3.1/2 from the 2003 C++ Standard: A local type, a type with no linkage, an unnamed type or a type compounded from any of these types shall not be used as a template-argument for a template type-parameter.
- ^ C++11標準:§14.7.3,¶16規定:the declaration shall not explicitly specialize a class member template if its enclosing class templates are not explicitly specialized as well
- ^ C++11標準:§14.6,¶1
- ^ C++11標準§14.6,¶2規定:A name used in a template declaration or definition and that is dependent on a template-parameter is assumed not to name a type unless the applicable name lookup finds a type name or the name is qualified by the keyword typename.
- ^ C++11標準§14.6,¶5規定
- ^ Template Instantiation. [2014-09-27]. (原始內容存檔於2021-02-26).