STL是C++中泛型編程(GP)的經典,而語言支持的模版語法是進行泛型編程的基礎。泛型編程是C++實現編譯時多態性的一種最重要的方式。這裡對C++語言的模版相關點進行一個總結,作為對深入理解泛型編程思想的基礎。
所謂模版(template)是一種藍圖或者公式,用來創建類或者函數。從抽象層次上看
- 類是對現實世界中同一類別有相同或相似特點的真實對象的一種抽象,定義相關數據成員,提供相關操作接口,通過類的實例化來表示現實世界真實對象從而進行軟件建模,這大大降低了代碼的重復。
- 模版是對相同或相似的類的一種更高層次的抽象,模版使用的類型參數就代表了這些類,它們可能有相似的功能,或者有相似的成員等等,通過實例化指定某一類型,就創建了一個類,這同樣大大降低了代碼的重復。
模版定義以關鍵字template開始,後接模版形參表,用一對尖括號包括一個或多個(逗號分隔)模版形參。模版形參以typename或者class關鍵字起始,class在舊的C++中就使用,typename是之後加入的,二者無其他差別。
函數模版的定義:
template
int cmp(const T & a, const T & b)
{
if (a < b) return -1;
else if (b < a) return 1;
else return 0;
}
類模版的定義:
template
class Stack
{
...
void push(const T & );
...
};
template
void Stack::push(const T &v)
{
...
}
模版類聲明完成之後,類的名稱就變為了 ClassName< T>,因此在類外定義成員函數時需要用上述語法進行域解析運算符聲明。
模版類型形參:
模版類型形參就是使用typename或者class指定的形參,這是需要在實例化階段指定特定類型,編譯器才能創建特定的類。此時,如果類定義了類型成員,如果直接使用類名+作用域解析運算符訪問,編譯器默認會當做一個數據成員,必須使用typename顯示告知編譯器才能作為一個類型成員。
template
void fun(A * a)
{
...
typename A::ref_type p;
...
}
上述定義假設泛型類A定義了類型成員ref_type
,那麼在模版函數或模板類中使用A的類型成員時,必須使用typename
,否則編譯器默認會將ref_type
當做A的一個數據成員對待。
模版非類型形參:
模版形參不一定都要是泛型的,可以是確定的某個類型,這是模版定義內部的常量值,在需要常量表達式的時候,可以使用非類型形參。
template
class Array{
private:
T arr[n];
public:
explict Array(const T &v);
...
};
這裡的非類型參數指定為特定的int類型,參數n就作為模版內部定義的一個常量,從而可以作為數據成員arr數組的大小。因此,非類型模版參數有如下限制:
- 非類型參數只能是整形、枚舉、引用、指針
- 模版定義內部不能修改非類型參數的值,也不能使用其地址(即不允許n++,&n等操作),因為必須將其當做為常量對待
- 模版實例化時必須使用常量或者常量表達式
對於上述定義的Array模板類,使用自動變量聲明的數組arr維護的是內存棧,與使用在構造函數通過動態傳入數組大小進行new/delete管理的堆內存相比,執行速度更快,對於頻繁操作的小的數組來說更明顯。但是,有一個缺點是,每種大小的數組編譯器都會重新生成獨立的類;另外,使用構造函數的進行動態分配方法更通用,可以在不同大小的數組間進行賦值等操作。
默認模版形參
當使用模版類型形參時,可按照函數默認值的規則,為模版類模版形參也提供默認值,默認值為某個特定的類型,必須放在非默認形參之後。但是,非類型的模版函數形參不能使用默認值。
template
class M
{
...
};
M c1; //c1 is M
M c2; //c2 is M
對於,非類型參數,模板類和模版函數均可以使用默認值。
template
class Array{
private:
T arr[n];
...
};
類模版本身是類的抽象,以泛型方式描述行為,需要進行實例化後才能由編譯器創建相應的類,從而創建對應類的對象並進行使用。編譯器用模板產生指定的類或函數的特定類型版本的實例的過程稱為實例化。分為隱式實例化和顯示實例化。
Stack ss; //隱式
template class Stack; //顯示聲明一個Stack類
隱式實例化時聲明類對象的同時指定模版參數,顯示實例化則是只指定模版參數從而實例化模板類,並沒有定義類對象。
通過實例化模版參數後,得到的Stack
這個整體作為一個從模版實例化得到的類,所有需要與該類相關的操作,都將這個作為一個整體對待。針對默認形參可以省略,或單獨指定。
對於模版非類型形參,需要使用常量或者常量表達式進行實例化,否則編譯器是不能通過的。
Array arrd;
對於函數模版,編譯器會根據調用函數傳遞的實參的類型自動實例化為相應類型的函數,這個過程稱為模版實參推斷(template argument deduction)。這在STL中的traits編程技法中充分進行了使用。
cmp(1, 11);
cmp(1.2, 2,4);
這兩次調用分別實例化了兩個函數:
int cmp(const int & a, const int & b)
{
if (a < b) return -1;
else if (b < a) return 1;
else return 0;
}
int cmp(const double & a, const double & b)
{
if (a < b) return -1;
else if (b < a) return 1;
else return 0;
}
在進行推斷時,有多個形參的實參必須完全匹配,類型推斷如上述示例,會為每種類型都產生一個函數。如果匹配錯誤則編譯出錯。另外,對於普通函數調用時實參的提示轉換,模版函數的匹配不會轉換實參,而是重新進行實例化產生新的函數,編譯器僅僅執行兩種轉換:
- const 轉換:接收const引用或者const指針的函數可以分別用非const對象的引用或者指針來調用,此時不會產生新的實例化。對於非引用類型形參,會忽略形參與實參的const。
- 數組或函數到指針的轉換:若模版形參不是引用類型,則對數組或函數類型的實參應用常規指針轉換,數組作為指向第一個元素指針,函數實參作為指向函數類型的指針。
template T f1(T, T);
template T f2(const T&, const T&);
string s1("s1");
const string s2("const s2");
f1(s1, s2); //調用 f1(string, string),忽略const
f2(s1, s2); //將s1轉換為const引用
int a[10], b[20];
f1(a, b); //調用f1(int *, int *),都視為指向首元素的指針
f2(a, b); //調用失敗,引用類型引用的兩個數組大小不相等,類型不匹配
另外,可以使用模版函數初始化函數指針,此時編譯器會使用函數指針聲明的類型作為模版實參來實例化模版函數:
template int cmp(const T&, const T&);
int (*pf) (const int &, const int &) = cmp;
顯示實參:
當函數返回值類型與形參表中的類型均不同時,需要使用顯示實參來解決這個問題:
temp1ate
Tl sum(T2 , T3);
temp1ate
T3 a1ternative_sum(T2 , Tl);
由於傳入T2和T3的類型未知,求和之後無法確定返回值為何類型,因此將返回類型也指定為了模版形參。此時僅僅調用sum函數來進行實參推斷時無法指定返回類型,因此需要顯示指定。使用顯示模版實參列表語法,編譯器從左到右依次匹配,最後順序匹配實參的類型。調用方法:
long va13 = sum (1, 10L);
long va12 = a1ternative_sum(1 , 10L);
暫時總結到此,後續總結類模版成員、成員模版以及模版特化等。