模板 (C++)
模板(Template)指C++程式語言中的函數模板與類模板[1],是一種參數化類型機制,大體對應於java和C#中的泛型,但也有一些功能上的顯著差異(C++模板支援後兩者沒有明確對應的模板模板參數和模板非類型參數,但不支援Java的萬用字元以及C#的泛型類型約束)。模板是C++的泛型編程中不可缺少的一部分。
模板定義以關鍵字template
開始,後接模板形參表(template parameter list),模板形參表是用尖括號括住的一個或者多個模板形參的列表,形參之間以逗號分隔。模板形參可以是表示類型的類型形參,也可以是表示常數表達式的非類型形參。非類型形參跟在類型說明符之後聲明。類型形參跟在關鍵字class或typename之後聲明。
使用模板時,可以在模板名字後面顯式給出用尖括號括住的模板實參列表(template argument list)。對模板函數或類的模板成員函數,也可不顯式給出模板實參,而是由編譯器根據函數呼叫的上下文推導出模板實參,這稱為模板參數推導。
模板是C++程式設計師絕佳的武器,特別是結合了多重繼承與運算子多載之後。C++的標準函式庫提供的許多有用的函數大多結合了模板的概念,如STL以及iostream。
簡介
函數模板
以下以取最大值的函數模板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;
//Calling template function
std::cout << maximum<int>(3,7) << std::endl; //输出 7
std::cout << maximum(3, 7) << std::endl; //自动补充类型声明
std::cout << maximum<double>(3.0,7.0) << 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++標準規定:如果外圍的類別模板沒有特例化,裏面的成員模板就不能特例化[2]。例如:
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)。[3]
編譯器在處理模板定義時,可能並不確定依賴名字表示一個類型,還是巢狀類的成員,還是類的靜態成員。C++標準規定:如果解析器在一個模板中遇到一個巢狀依賴名字,它假定那個名字不是一個類型,除非顯式用typename關鍵字前置修飾該名字。[4]
typename關鍵字有兩個用途:
- 常見的在模板定義中的模板形參列表,表示一個模板參數是類型參數。等同於使用class。
- 使用模板類內定義的巢狀依值型別名字時,顯式指明這個名字是一個類型名。否則,這個名字會被理解為模板類的靜態成員名。
在下述情形,對巢狀依值型別名字不需要前置修飾typename關鍵字:[5]
- 衍生類別聲明的基礎類別列表中的基礎類別識別碼;
- 成員初始化列表中的基礎類別識別碼;
- 用
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關鍵字有兩個用途:
- 常見的在模板定義的開始。
- 是模板類內部定義了模板成員函數或者巢狀的成員模板類。當參照這樣的模板成員函數或巢狀的成員模板類時,可以在
::
(作用域解析)運算子、.
(以對象方式訪問成員)運算子、->
(以指標方式訪問成員)運算子之後是模板成員函數名字或巢狀的成員模板類名字,隨後的左尖括號<
將被解釋為模板參數列的開始。例如:
#include <iostream>
using namespace std;
template <class T>
class A{
template <class U>
class B{
public:
typedef int INT;
};
public:
template <class V>
void foo(){};
};
int main()
{
A<float>::B<double>::INT i;
i=101;
A<bool> a, *p=&a;
a.foo<char>();
p->foo<long>();
return 0;
}
模板實例化
模板實例化(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:巢狀的類別模板
- ^ 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].