目錄
- 模闆與泛型程式設計
- 16.1 定義模闆
- 16.1.1 函數模闆
- 16.1.2 類模闆
- 16.1.3 模闆參數
- 16.1.4 成員模闆
- 16.1 定義模闆
OOP,能處理類型在程式運作之前都未知的情況;泛型程式設計,在編譯時能擷取類型。
模闆是泛型程式設計的基礎。本章學習如何定義自己的模闆。
問題引出:假設希望編寫一個函數來比較2個值,并指出第一個值是<, > or == 第二個值。實際程式設計中,可能想要定義多個重載函數,每個函數比較一種給定類型的值。這樣就會寫很多函數體一樣的函數,而僅僅是函數類型不同,很繁瑣。我們使用函數模闆解決這個問題。
// 下面2個函數用于比較v1和v2的大小,僅僅是函數參數類型不一樣,函數體完全一樣
// string版本
int compare(const string &v1, const string &v2) {
if (v1 < v2) return -1;
else if(v1 > v2) return 1;
return 0;
}
// double版本
int compare(const double&v1, const double&v2) {
if (v1 < v2) return -1;
else if(v1 > v2) return 1;
return 0;
}
以形如template 開始,為函數定義類型參數,可以執行個體化出特定函數的模闆,就是函數模闆。
類型參數T,可以看作類型說明符,作為函數傳回值類型或者形參類型。會在調用函數時,編譯器利用實參類型推斷出T代表的類型。
什麼叫(函數模闆)執行個體化?
當調用一個函數時,編譯器用函數實參推斷出的模闆參數,用此實際實參代替模闆參數來建立出一個新的“執行個體”,也就是一個真正可以調用的函數,這個過程叫執行個體化。
編譯器生成的函數版本,通常稱為模闆的執行個體。
// 定義compare的函數模闆
// compare聲明了類型為T的類型參數
template <typename T> // template關鍵字, <typename T> 是模闆參數清單,typename和class關鍵字等價,都可以使用,T是模闆參數,以逗号分隔其他模闆參數
int compare(const T &v1, const T &v2) {
if (v1 < v2) return -1;
else if (v1 > v2) return 1;
return 0;
}
// 調用模闆,将模闆實參綁定到模闆參數(T)上
// 調用函數模闆時,編譯器根據函數實參(1,0)來推斷模闆實參
cout << compare(1, 0) << endl; // T為int
// 編譯器會根據調用情況,推斷出T為int,進而生成一個compare版本,T被替換為int
int compare(const int &v1, const int &v2) {
if (v1 < v2) return -1;
else if (v1 > v2) return 1;
return 0;
}
模闆類型參數
類型參數可以看做類型說明符,像内置類型或類類型說明符一樣使用,單僅限于定義模闆的函數傳回類型、參數類型、函數體内變量聲明、類型轉換。
類型參數前必須使用關鍵字typename或class,兩者等價,可互換。(僅限于模闆參數清單中)
template <typename T> T foo(T *p) {
T tmp = *p; // tmp類型T,是指針p指向的類型
// ...
return tmp;
}
// 錯誤使用示例
template <typename T, U> T calc(const T&, const U&); // U 之前必須加typename或class
// 正确
template <typename T, class U> T calc(const T&, const U&);
非類型模闆參數
非類型參數表示一個值,而非一個類型。通過一個特定類型名而非(typename/class)來指定非類型參數。
當一個模闆執行個體化時,非類型參數被使用者提供的,或編譯器推斷出的值所代替。這些值必須是常量表達式。
注意:
- 綁定到非類型整型參數的實參必須是一個常量表達式;
- 綁定到指針或引用非類型參數的實參必須具有靜态生存期;
template <unsinged N, unsigned M> // N, M是非類型整型參數
int compare(const char &(p1)[N], const char &(p2)[M]) {
return strcmp(p1, p2);
}
// 調用compare時,編譯器會用字面量大小來替代非類型參數N和M
compare("hi", "mom"); // N = 3, M = 4,注意編譯器會自動在字元串末尾添加"\0"作為終結符
inline和constexpr的函數模闆
聲明inline或constexpr的函數模闆,inline/constexpr說明符要放在模闆參數清單之後,傳回類型之前:
// 正确,inline放在template模闆參數之後,傳回值類型之前
template <typename T> inline T min(const T&, const T&);
// 錯誤,inline放到了template之前
inline template <typename T> T min(const T&, const T&);
編寫類型無關的代碼*
前面compare函數,說明了編寫泛型代碼的2個重要原則:
- 模闆中的函數是const引用
- 函數體中條件判斷僅使用< 比較運算
但是,編寫代碼如果使用了 <, >運算符,就降低了compare對要處理類型的要求。也就是說,這些類型必須要支援<,>。
如果真的關心類型無關和可移植性,可能需要用到less(标準庫函數,頭檔案 algorithm)來定義compare函數。
// 實際上less函數也用到了<,并沒有起到更良好定義的作用
template <typename T> int compare(const T &v1, const T&v2) {
if (less<T>()(v1, v2)) return -1; // <=> if(v1 < v2)
else if(less<T>()(v2, v1)) return 1;
return 0;
}
模闆編譯*
編譯器在模闆定義時,不生成代碼。隻有執行個體化出模闆的一個特定版本時,編譯器才會生成代碼。
函數模闆和類模闆成員函數的定義通常放在頭檔案中。
可以執行個體化出特定類的模闆,叫類模闆。
類模闆是用來生成類的藍圖的。與函數模闆的差別是,編譯器不能為類模闆推斷模闆參數類型。
template <typename T> class Blob { // 類型為T的模闆類型參數
public:
typedef T value_type;
typedef typename std::vector<T>::size_type size_type;
// 構造函數
Blob();
Blob(std::initializer_list<T> il);
// Blob中的元素數目
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
// 添加和删除元素
void push_back(const T &t) { data->push_back(t); }
// 移動版本
void push_back(T &&t) { data->push_back(std::move(t));}
void pop_back();
// 元素通路
T& back();
T& operator[](size_type i);
private:
std::shared_ptr<std::vector<T>> data;
// 若data[i]無效,則抛出msg異常資訊
void check(size_type i, const std::string &msg) const;
};
執行個體化類模闆
要使用類模闆,必須提供額外資訊,即顯示模闆實參清單,綁定到模闆參數。編譯器可以用這些模闆實參執行個體化出特定的類。
一個類模闆的每個執行個體都是一個獨立的類,比如Blob與Blob沒有任何關聯,也不會對任何其他Blob類型的成員有特殊通路權限。
// 使用特定類型版本的Blob(即Blob<int>),必須提供元素類型
Blob<int> ia; // 建構空Blob<int>
Blob<int> ia2 = {0,1,2,3,4}; // 建構包含5個元素的Blob<int>
// 使用Blob<string> 版本
Blob<string> names;
// 使用Blob<double> 版本
Blob<double> prices;
編譯器執行個體化出一個與下面定義等價的類:
// 注意:所有模闆參數T都被編譯器根據顯式模闆實參,替換為對應的類型
template<> class Blob<int> {
typedef typename std::vector<int>::size_type size_type;
Blob();
Blob(std::initializer_list<int> il);
// ...
int& operator[](size_type i);
private:
std::shared_ptr<std::vector<int>> data;
void check(size_type i, const std::string &msg) const;
}
在模闆作用域中引用模闆類型
類模闆的名字不是一個類型名。類模闆用來執行個體化類型,而一個執行個體化的類型總是包含模闆參數的。
也就是說,template class Blob{...} 這裡模闆名稱Blob不是一個類型名,而模闆參數T當做被使用模闆的實參。
簡而言之,就是類模闆參數T,可以在類内部成員定義時使用,而T所代表的類型取決于執行個體化Blob傳入的類型(xxx)。
// data定義,使用了Blob的類型參數T,來聲明data是一個share_ptr的執行個體
std::shared_ptr<std::vector<T>> data;
// 執行個體化特定類型Blob<string>後,data成為
shared_ptr<vector<string>> data;
類模闆的成員函數
類模闆的成員函數是一個普遍函數,每個執行個體化的類,都有自己版本的成員函數。
如check, back, operator[]
template<typename T>
void Blob<T>::check(Blob::size_type i, const std::string &msg) const { // 檢查目前位置i是否合法
if (i >= data->size()) throw std::out_of_range(msg);
}
template<typename T>
T &Blob<T>::back() {
check(0, "back on empty Blob");
return data->back();
}
template<typename T>
T &Blob<T>::operator[](Blob::size_type i) {
// 如果i太大,check抛出異常,阻止通路不存在的元素
check(i, "subscripte out of range");
// return data[i]; // 錯誤,data是一個指向vector<T>的shared_ptr,vector下标通路需要先解引用
return (*data)[i];
}
template<typename T>
void Blob<T>::pop_back() { // 彈出末尾元素
// 檢查data指向的vector是否為空
check(0, "pop_back on empty Blob");
data->pop_back();
}
構造函數
template<typename T>
Blob<T>::Blob() : data(std::make_shared<std::vector<T>>()){ // 構造函數
}
template<typename T>
Blob<T>::Blob(std::initializer_list<T> il) : data(std::make_shared<std::vector<T>>(il)) { // 初始化清單構造函數
}
// 使用了上面的構造函數,Blob對象就能像下面這樣構造
Blob<string> articles = {"a", "an", "the"};
類模闆成員函數的執行個體化
預設情況下,類模闆成員函數隻有當程式用到它時才執行個體化。
類内、類外使用模闆類名*
類的作用域内,可以直接使用模闆名而不必指定模闆實參.。
// 注意模闆名後面的類型參數清單<T>
// 類内可以使用簡化名稱
Blob &Blob(Blob &&); // 移動構造函數
Blob &operator++(); // 前置自增 <=> Blob<T> &operator()
// 類外定義成員時,不在類的作用域,要指出類型參數T
template <typename T>
Blob<T> Blob<T>::operator++(int); // 後置自增
類模闆和友元
當一個模闆類包含一個友元聲明時,類與友元各自是否模闆無關?
如果一個類模闆包含一個非模闆友元,則友元被授權可以通路所有模闆執行個體。 如果友元自身是模闆,類可以授權所有友元模闆執行個體,也可以隻授權給特定執行個體。
一對一友好關系
引用(類或資源)模闆的一個特定執行個體
步驟:
- 聲明模闆自身;
- 在類内聲明友元關系;
// 注意1對1友元關系中,友元聲明和類模闆本身不同之處
// 前置聲明,在Blob中聲明友元所需要
template <typename> class BlobPtr; // 聲明友元函數需要
template <typename> class Blob; // 運算符== 參數清單需要
template <typename T> bool operator==(const Blob<T> &, const Blob<T> &); // 聲明要作為友元的函數
template <typename T> class Blob {
friend class BlobPtr<T>; // 每個Blob執行個體将通路權限授予用相同類型執行個體化的BlobPtr和相等運算符
freind bool operator==<T>(const Blob<T> &, const Blob<T> &);
//其他成員定義
...
};
通用和特定的模闆友好關系
一個類将另一個類聲明為友元,情況分為兩大類:
1.非模闆類中,聲明友元類:聲明的類可以是模闆類,也可以是非模闆類(普通友元聲明);
2.模闆類中,聲明友元類:聲明的類可以模闆類,也可以是非模闆類;
// 前置聲明,在C和C2中聲明友元所需
template <typename T> class Pal;
// 注意這裡沒有Pal2的前置聲明
class C{ // C是一個普遍非模闆類
friend class Pal<C>; // (用類C)執行個體化的Pal是C的一個友元,1對1友元關系。
template <typename T> friend class Pal2; // Pal2所有執行個體都是C的友元, 因為已經包含了模闆參數清單, 不需前置聲明
};
template <tyepname T> class C2 { // C2是一個模闆類
friend class Pal<T>; // C2的每個執行個體,将相同執行個體化的Pal聲明為友元
template <typename X> friend class Pal2; // Pal2的所有執行個體都是C2的友元,不需要前置聲明。這裡X代表Pal2使用的模闆參數,跟C2使用的T不一樣
friend class Pal3; // Pal3是非模闆類,是C2所有執行個體的友元。不需要前置聲明
};
令模闆自己的類型參數成為友元
模闆類可以将自己的類型參數,聲明為友元
template <typename T> class Bar {
friend T; // 将類的通路權限,授予用來執行個體化的Bar類型 (模闆類執行個體化後的類)
// ...
};
模闆類型别名
用typedef定義引用執行個體化的類的别名,用using定義引用模闆類的别名。
typedef Blob<string> StrBlob; // 正确,引用的是Blob<string>,屬于模闆的一個執行個體,StrBlob是Blob<string>的别名
typedef Blob<T> StrBlob; // 錯誤,由于Blob<T>模闆不是一個類型,不能用typedef引用一個模闆類
template <typename T> using StrBlob = Blob<T>; // 正确,StrBlob是模闆類的别名
StrBlob<int> b1; // <=> Blob<int>
StrBlob<double> b2; // <=> Blob<double>
StrBlob<string> b3; // <=> Blob<string>
類模闆的static成員
所有執行個體化的類,都包含自己的static成員。
如下面的類模闆,一個給定的執行個體化的類Foo,包含一個共同的static 成員。不同的執行個體化的類,包含不同的static成員。
template <typename T> class Foo {
public:
static std::size_t count() { return ctr; } // static函數成員
// ...
private:
static std::size_t ctr; // static資料成員
// ...
};
類似函數參數的名字,模闆參數的名字隻是一個符号,沒有什麼含義,T隻是習慣上的命名。
模闆參數與作用域
模闆參數的作用域從聲明之後,到模闆聲明/定義結束之前。而且,模闆内不能重用模闆參數名。
typedef double A;
template <typename A, typename B> void f(A a, B b) // 模闆參數A,B的作用域從聲明之後,到模闆聲明/定義結束之前
{
A tmp = a; // 覆寫了typedef對A的定義,A代表的類型由函數模闆執行個體化決定
double B; // 錯誤:模闆參數名不能重用
};
模闆聲明
聲明,但不定義模闆,不過必須包含模闆參數。聲明和定義中的參數名稱,不必相同。
// 聲明,但不定義模闆
template <typename T> int compare(const T&, const T&);
template <typename T> class Blob;
// 3個聲明/定義都指向相同的函數模闆
template <typename T> T calc(const T&, const T&); // 聲明
template <typename U> U calc(const U&, const U&); // 聲明
template <typename X> X calc(const X& a, const X& b) { // 定義
// ...
}
使用類的類型成員
如何通過類模闆參數T,使用執行個體化之後T内定義的類型?
如果直接用作用域運算符(::)這樣做,編譯器無法判斷是想使用T的靜态成員value_type,還是想使用T内類型valuetype。
// 聲明類型的錯誤方式
T::value_type
// 聲明類型的正确方式
template <typename T>
typename T::value_type top(const T& c) { // 注意這裡的typename T::value表明這是一個類型
if(!c.empty()) return c.back();
else return typename T::value_type(); // 疑問:這裡如果T::value_type表示類型,為何會帶一個() ? 答案是這裡的 類型+(),會調用預設構造函數(對類)或者内置的初始化方法(對内置類型,如int,初值一般為0)
};
template <typename T, typename F = less<T>> // F預設值是less<T>,一個模闆類,重載了函數調用運算符(operator())
int compare(const T &v1, const T &v2, F f = F()) { // 這裡F(),是相當于調用less<T>(),也就是less<T>的函數調用重載版本
if (f(v1, v2)) {
return -1;
}
if (f(v2, v1)) {
return 1;
}
return 0;
}
// 調用函數模闆執行個體
auto i = compare(0, 42); // i = -1