第十六章 模板與泛型編程
定義模板
1. 函數模板
模板定義以關鍵字template關鍵字開始,后面跟著一個模板參數列表(不能為空):
template <typename T>
int compare(const T &v1, const T &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
類型參數可以用來指定返回類型或函數的參數類型,以及在函數體內用于變量聲明或類型轉換:
template <typename T> T foo(T* p) {
T tmp = *p; // tmp的類型是指針p指向的類型
// ...
return tmp;
}
1.1 非類型模板參數
除了定義類型參數,還可以定義非類型參數nontype parameter
,一個非類型參數表示一個值而非一個類型。當一個模板被實例化時,非類型參數被一個用戶提供的或者編譯器推斷出來的值鎖替代,這些紙必須是常量表達式,從而允許編譯器在編譯時實例化模板。
比如我們編寫一個compare
版本處理字符串字面常量(const char
的數組),由于不能拷貝數組,因此我們將自己的參數定義為數組的引用,由于我們希望能夠比較不同長度的字符串字面常量,因此為模板定義了兩個非類型的參數:第一個模板參數表示第一個數組長度,第二個參數表示第二個數組的長度:
template<unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M])
{
return strcmp(p1, p2);
}
// 調用
compare("hi", "mom");
// 編譯器相當于實例化如下版本:
int compare(const char (&p1)[3], const char(&p2)[4])
- 非類型參數可以是一個整型,或者是一個指向對象或者函數類型的指針或(左值)引用
- 綁定到非類型參數的實參必須是一個常量表達式
- 綁定到指針或者引用非類型參數的實參必須具有靜態的生存期
1.2 編寫類型無關的代碼
編寫泛型代碼的兩個重要原則:
- 模板中的函數參數是
const
的引用 - 函數體中的條件判斷僅使用
<
比較運算
通過將函數參數設定為const
引用,我們保證函數可以用于不能拷貝的類型。大多數類型(除了unique_ptr
和IO
類型外)都是允許拷貝的,但是不允許拷貝的類型也是存在的,而且使用常量引用在處理大對象時也可以使函數運行地更快。
1.3 模板編譯
當編譯器遇到一個模板定義時,它并不生成代碼。只有當我們實例化出模板的一個特定版本時,編譯器才會生成代碼。即當我們使用而非定義模板時,編譯器才生成代碼。
通常當我們調用一個函數時,編譯器只需要掌握函數的聲明。類似地,當我們使用一個類類型的對象時,類定義必須是可用的,但是成員函數的定義不必已經出現。因此我們通常將類定義和函數聲明放在頭文件中,而普通函數和類的成員函數的定義放在源文件中。
為了生成一個實例化版本,編譯器需要掌握函數模板或者類模板成員函數的定義。因此與非模板文件不同,模板的頭文件通常既包含聲明也包含定義。
模板直到實例化時才會生成代碼,這一特性影響了我們何時才會直到模板內代碼的編譯錯誤:
- 第一個階段:編譯模板本身。編譯器只能檢查語法錯誤,比如忘記分號或者變量名拼錯。
- 第二個階段:編譯器遇到模板使用時。對于函數模板調用,編譯器通常會檢查實參數目是否正確,還能檢查參數類型是否匹配;對于類模板,編譯器可以檢查用戶是否提供了正確數目的模板實參
- 第三個階段:模板實例化。只有這個階段才能發現類型相關的錯誤,這類錯誤可能在鏈接時才報告。
2. 類模板
類模板class template
使用來生成類的藍圖的。
2.1 定義類模板
template <typename T> class Blob {
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:
// Q: 為什么使用智能指針?
std::shared_ptr<std::vector<T>> data;
// 若data[i]無效,則拋出msg
void check(size_type i, const std::string &msg) const;
};
2.2 實例化類模板
當使用一個類模板時,我們必須提供額外信息,即顯式模板實參explicit template argument
,編譯器使用這些模板實參來實例化出特定的類。
Bolb<int> ia;
Blob<int> ia2 = {0,1,2,3,4};
2.3 在模板作用域內引用模板類型
舉個例子,我們的data
成員使用了兩個模板:vector
和shared_ptr
,我們將其定義為:
std::shared_ptr<std::vector<T>> data;
2.4 類模板的成員函數
Blob
的成員函數應該這么定義:
template <typename T>
ret-type Bolb<T>::member-name(param-list)
check
和元素訪問成員:
template <typename T>
void Bolb<T>::check(size_type i, const std::string &msg) const
{
if (i >= data->size())
throw std::out_of_range(msg);
}
2.5 Bolb構造函數
template <typename T>
// 構造函數分配一個空vector, 并將指向vector的指針保存在data中
Bolb<T>::Blob() : data(std::make_shared<std::vector<T>>()) { }
// 接受一個initializer_list參數的構造函數將其類型參數為T作為initializer_list參數的元素類型
template <typename T>
Blob<T>::Blob(std::initializer_list<T> il) : data(std::make_shared<std::vector<T>>(il)) { }
// 使用方法: 在這條語句中構造函數的參數類型為initializer_list<string>,列表中每個字符串字面常量會被隱式地轉換為一個string
Blob<string> articles = {"a", "an", "the"};
2.6 類模板成員函數的實例化
默認情況下一個類模板的成員函數只有當程序用到它時才進行實例化,成員函數只有被用到時才進行實例化,這一特性使得即使某種類型不能完全符合模板操作的要求,我們仍然能用該類型實例化類。
2.7 在類模板的作用域內簡化模板類名的使用
// 后置: 遞增/遞減對象但返回原值
template <typenname T>
BlobPtr<T> BlobPtr<T>::operator++(int)
{
// 此處無須檢查, 調用前置遞增時會進行檢查
BlobPtr ret = *this; // 保存當前值, 這里等價于BlobPtr<T> ret = *this; 在類模板作用域內簡化模板類名的使用
++ *this; // 推進一個元素, 前置++檢查遞增是否合法
return ret; // 返回保存的狀態
}
2.8 類模板和友元
類模板和另一個(類或者函數)模板間友好關系最常見的形式是建立對應實例及其友元間的友好關系:
// 首先將Blob、BlobPtr和operator==聲明為模板, 這些聲明是operator==函數的參數聲明及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 {
// 每個Blob實例將訪問權限授予相同類型實例化的BlobPtr和相等運算符
friend class BlobPtr<T>;
friend bool operator==<T>
(const Blob<T>&, const Blob<T>&);
}
// 友元的聲明用Blob的模板形參作為他們自己的模板形參,因此友好關系被限定在用相同類型實例化的Blob與BlobPtr相等運算符之間
Blob<char> ca; // BlobPtr<char>和operator==<char>都是本對象的友元
Blob<int> ia; // BlobPtr<int>和operator==<int>都是本對象的友元
// BlobPtr<char>的成員可以訪問ca(或者任意其他Blob<char>對象)的非public部分, 但對ia或任何其他Blob<int>對象或Blob的其他實例都沒有特殊訪問權限
一個類也可以將另一個模板的每個實例都聲明為自己的友元,或者限定特定的實例為友元:
// 前置聲明, 在將模板的一個特定實例聲明為友元時將要用到
template <typename t> class Pal;
class C { // C時一個普通的非模板類
friend class Pal<C>; // 用類C實例化的Pal是C的一個友元
// Pal2的所有實例都是C的友元, 這種情況無須前置聲明
template <typename T> friend class Pal2;
};
template <typename T> class C2 { // C2本身是一個類模板
// C2的每個實例將相同實例化的Pal聲明Pal聲明成友元
friend class Pal<T>; // Pal的模板聲明必須在作用域之內
// Pal2的所有實例都是C2的每個實例的友元,不需要前置聲明
template <typename X> friend class Pal2;
// Pal3是一個非模板類,它是C2所有實例的友元
friend class Pal3;
}
在新標準中,我們可以將模板類型參數聲明為友元,這樣對于某個類型名Foo
,Foo
將成為Bar<Foo>
的友元:
template <typename Type> class Bar {
friend Type; // 將訪問權限授予用來實例化Bar的類型
// ...
}
2.9 模板類型別名
我們可以通過定義一個typedef
來引用實例化后的類:
typedef Blob<string> StrBlob;d
由于模板并不是一個類,因此我們不能定義一個typedef
引用Blob<T>
,但是新標準允許我們為類模板定義一個類型別名:
template <typename T> using twin = pair<T, T>;
twin<string> authors; // authors是一個pair<string, string>
// 我們也可以固定一個或者多個模板參數
template <typename T> using partNo = pair<T, unsigned>;
partNo<string> books; // books類型是pair<string, unsigned>
2.10 類模板與static成員
類模板可以聲明static成員:
template <typename T> class Foo {
public:
static std::size_t count() { return ctr; }
// 其他接口成員
private:
static std::size_t ctr;
};
每個Foo
的實例都有其自己的static
成員實例,即給定任意類型X
,都有一個Foo<X>::ctr
和Foo<X>::count
成員,所有的Foo<X>
類型的對象共享相同的ctr
對象和count
函數。由于類的每個實例都有一個獨有的static
對象,因此我們可以將它的static
成員也定義成模板:
template <typename T>
size_t Foo<T>::ctr = 0;
3. 模板參數
3.1 模板聲明
一個特定文件所需要的所有模板的聲明通常一起放置在文件開始位置,出現于任何使用這些模板的代碼之前,原因我們將在后面講。
3.2 使用類的類型成員
我們用作用域運算符::
來訪問static
成員和類型成員,在普通(非模板)代碼中,編譯器掌握類的定義,因此它直到通過作用域運算符訪問的名字是類型還是static
成員。比如當我們寫下string::size_type
,由于編譯器有string
的定義,因此它知道size_type
是一個類型。
對于模板代碼來說就不是這么簡單,假定T
是一個模板類型參數,當編譯器遇到T::mem
代碼時,它在實例化之前不知道mem
是一個類型成員還是一個static
數據成員。比如編譯器遇到如下語句時:
T::size_type *p
// 編譯器需要知道我們是正在定義一個名為`p`的變量,還是將一個名為size_type的static數據成員與名為p的變量相乘
如果我們希望使用一個模板類型參數的類型成員,就必須顯式告訴編譯器該名字是一個類型,我們通過使用關鍵字typename
來實現這一點:
template <typename T>
typename T::value_type top(const T& c)
{
if (!c.empty())
return c.back();
else
return typename T::value_type(); // 使用T的類型
}
3.3 默認模板實參
在新標準中我們可以為函數和類模板提供默認實參:
// compare有一個默認模板實參less<T>和一個默認函數實參F()
template <typename T, typename F = less<T>>
int compare(const T &v1, const T &v2, F f = F())
{
if (f(v1, v2)) return -1;
if (f(v2, v1)) return 1;
return 0;
}
與函數默認實參一樣,對于一個模板參數,只有它右側的所有參數都有默認實參時,它才可以有默認實參。
3.4 模板默認實參與類模板
如果一個類模板為其所有模板參數都提供了默認實參,且我們希望使用這些默認實參,就必須在模板名后面加上一個空尖括號對:
template <class T = int> class Numbers { // T 默認為int
public:
Numbers(T v = 0) : val { }
// 其他操作
private:
T val;
};
Number<long double> lots_of_precision;
Number<> average_precision; // 空<>表示我們希望使用默認類型
4. 成員模板
一個類可以包含本身是模板的成員函數,這種成員被稱為成員模板,成員模板不能是虛函數。
4.1 普通(非模板)類的成員模板
我們定義一個類,類似于unique_ptr
所使用的默認刪除器類型。我們的類將包含一個重載的函數調用運算符,它接受一個指針并對此指針執行delete
,由于我們希望刪除器適用于任何類型,因此我們將調用運算符定義為一個模板。
// 函數對象類. 對給定指針執行delete
class DebugDelete {
public:
DebugDelete(std::ostream &s = std::cerr) : os(s) { }
// 與任何函數模板相同, T的類型由編譯器推斷
template <typename T> void operator()(T *p) const
{ os << "deleting unique_ptr" << std::endl; delete p; }
private:
std::ostream &os;
}
我們可以使用這個類代替delete
函數:
double* p = new double;
DebugDelete d; // 可像delete表達式一樣使用的對象
d(p); // 調用DebugDelete::operator()(double*), 釋放p
int* ip = new int;
// 在一個臨時DebugDelete對象上調用operator()(int*)
DebugDelete()(ip);
我們可以將DebugDelete
用作unique_ptr
的刪除器:
// 刪除p指向的對象
// 實例化DebugDelete::operator()<int>(int *)
unique_ptr<int, DebugDelete> p(new int, DebugDelete());
//銷毀sp執行的對象
unique_ptr<string, DebugDelete> sp(new string, DebugDelete());
4.2 類模板的成員模板
對于類模板,我們也可以定義它的成員模板,這種情況下,類和成員各自有自己的、獨立的模板參數。
template <typename T> class Blob {
// 構造函數是一個成員模板, 接受兩個迭代器, 表示要拷貝的元素范圍
template <typename It> Blob(It b, It e);
// ...
};
// 當我們在類模板外定義一個成員模板時, 必須同時為類模板和成員模板提供模板參數列表, 類模板的參數列表在前, 后跟成員的模板參數列表
template <typename T> // 類的類型參數
template <typename It> // 構造函數的類型參數
Blob<T>::Blob(It b, It e) : data(std::make_shared<std::vector<T>>(b, e)) { }
4.3 實例化與成員模板
為了實例化一個類模板的成員模板,我們必須同時提供類和函數模板的實參。與普通函數模板相同,編譯器通常根據傳遞給成員模板的函數實參來推斷它的模板實參:
int ia[] = {0,1,2,3,4,5,6,7,8,9};
vector<long> vi = {0,1,2,3,4,5,6,7,8,9};
list<const char*> w = {"now", "is", "the", "time"};
// 實例化Blob<int>類及其接受兩個int*參數的構造函數
Blob<int> a1(begin(ia), end(ia));
// 實例化Blob<int>類及其接受兩個vector<long>::iterator的構造函數
Blob<int> a2(vi.begin(), vi.end());
// 實例化Blob<string>及其接受兩個list<const char*>::iterator參數的構造函數
Blob<string> a3(w.begin(), w.end());
5. 控制實例化
前面我們提到只有當模板被使用時才會進行實例化,這一特性意味著相同的實例可能出現在多個對象文件中。
當多個獨立編譯的源文件使用了相同的模板,并且提供了相同的模板參數時,每個文件中就都會有該模板的一個實例。在大系統中,如果我們在多個文件中實例化相同模板的額外開銷可能非常嚴重。
新標準允許我們通過顯式實例化explicit instantiation
來避免這種開銷。
extern template declaration; // 實例化聲明
template declaration; // 實例化定義
例子:
// 實例化聲明與定義
extern template class Blob<string>; // 聲明
template int compare(const int&, const int&); // 定義
當編譯器遇到extern
模板聲明時,他不會在本文件中生成實例化代碼,將一個實例化聲明為extern
就表示承諾在程序其他位置有該實例化的一個非extern
聲明(定義)。對于一個給定的實例化版本,可能有多個extern
聲明,但必須只有一個定義。對于一個給定的實例化版本,可能有多個extern聲明,但必須只有一個定義。
由于編譯器在使用一個模板時自動對齊實例化,因此extern
聲明必須出現在任何使用此實例化版本的代碼之前:
// Application.cc
// 這些模板類型必須在程序其他位置進行實例化
extern template class Blob<string>;
extern template int compare(const int&, const int&);
Blob<string> sa1, sa2; // 實例化會出現在其他位置
// Blob<int>及其接受initializer_list的構造函數在本文件中實例化
Blob<int> a1 = {0,1,2,3,4,5,6,7,8,9};
Blob<int> a2(a1); // 拷貝構造函數在本文件中實例化
int i = compare(a1[0], a2[0]); // 實例化出現在其他位置
文件Application.o
將包含Blob<int>
的實例及其接受initializer_list
參數的構造喊你書和拷貝構造函數的實例。而compare<int>
函數和Blob<string>
將不在本文件中進行實例化,這些模板的定義必須出現在程序的其他文件中:
// templateBuild.cc
// 實例化文件必須為每個在其他文件中聲明為extern的類型和函數提供一個非extern的定義
template int compare(const int&, const int&);
template class Blob<string>; // 實例化類模板的所有成員
當編譯器遇到一個實例化定義(與聲明相對)時,它為其生成代碼。因此,文件templateBuild.cc
將會包含compare
的int
實例化版本的定義和Blob<string>
類的定義。當我們編譯此應用程序時,必須將templateBuild.o
和Application.o
鏈接到一起。
一個類模板的實例化定義會實例化該模板的所有成員,包括內聯的成員函數。與處理類模板的普通實例化不同,編譯器會實例化該類的所有成員。即使我們不使用某個成員,它也會被實例化。因此在一個類模板的實例化定義中,所用類型必須能用于模板的所有成員函數。
模板實參推斷
1. 類型轉換與模板類型參數
能在調用中應用于函數模板的包括如下三項:
頂層
const
無論是在形參中還是在實參中都會被忽略const
轉換:可以將一個非const
對象的引用(或指針)傳遞給一個const
的引用(或指針)形參數組或函數指針轉換:如果函數形參不是引用類型,則可以對數組或函數類型的實參應用正常的指針轉換。一個數組形參可以轉換為一個指向其首元素的指針,一個函數實參可以轉換為一個該函數類型的實參
將實參傳遞給待模板類型的函數形參時,能夠自動應用的類型轉換只有
const
轉換及數組或函數到指針的轉換。
1.1 使用相同模板參數類型的函數形參
// compare函數接受兩個const T&參數, 其實參必須是相同類型
long lng;
compare(lng, 1024); // 錯誤, 不能實例化compare(long, int)
1.2 正常類型轉換應用于普通函數參數
template <typename T> ostream &print(ostream &os, const T &obj)
{
return os << obj;
}
// 由于低于一個參數的類型不依賴于模板參數, 因此編譯器會將f隱式轉換為ostream&
ofstream f("output");
print(f, 10);
2. 函數模板顯式實參
假設我們定義一個sum
的函數模板,它接收兩個不同類型的參數,我們希望允許用戶指定結果的類型,這樣用戶就可以選擇合適的精度。我們可以定義表示返回類型的第三個模板參數,從而允許控制返回類型:
// 編譯器無法推斷T1, 它不會出現在函數參數列表中
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3)
每次調用sum
時調用者都必須為T1
提供一個顯式模板實參:
auto val3 = sum<long long>(i, lng); // long long sum(int, long)
需要注意的是,顯式模板實參按由左到右的順序與對應的模板參數匹配:第一個模板實參與第一個模板參數匹配,第二個實參與第二個參數匹配。只有尾部參數的顯示模板實參才可以忽略,而且前提是它們可以從函數參數推斷出來。如果我們的sum
函數按照如下形式編寫:
// 糟糕的設計, 用戶必須指定所有三個模板參數
template <typename T1, typename T2, typename T3>
T3 alternative_sum(T1, T2);
// 錯誤: 不能推斷前幾個模板參數
auto val3 = alternative_sum<long long>(i, lng);
// 正確: 顯式指定了所有三個參數
auto val2 = alternative_sum<long long, int, long>(i, lng);
正常類型轉換應用于顯式指定的實參:
long lng;
compare(lng, 1024); // 模板參數不匹配
compare<long>(lng, 1024); // 正確: 實例化compare(long, long);
compare<int>(lng, 1024); // 正確: 實例化compare(int, int);
3. 尾置返回類型與類型轉換
3.1 尾置返回類型
當我們希望用戶確定返回類型時,用顯式模板實參表示模板函數的返回類型是比較有效的,但是要求顯式指定模板實參會給用戶增添額外負擔。尾置返回允許我們在參數列表之后聲明返回類型:
template <typename It>
auto fcn(It beg, It end) -> decltype(*beg) // 通知編譯器fcn的返回類型與解引用beg參數的結果類型相同, 解引用類型返回一個左值, 因此通過decltype推斷的類型為beg表示的元素的類型的引用
{
// 處理負擔
return *beg; // 返回序列中一個元素的引用
}
3.2 進行類型轉換的標準庫模板類
在前面提到的例子中,我們對傳遞的參數類型一無所知,唯一可以使用的操作是迭代器操作,而所有的迭代器操作都不會生成元素,只能生成元素的引用。
為了獲得元素類型,我們可以使用標準庫的類型轉換
type transformation
模板,這些模板定義在type_traits
中。
在本例中,我們可以使用remove_reference
來獲得元素類型。remove_reference
模板有一個模板類型參數和一個名為type
的public
類型成員。如果我們用一個引用類型實例化它,那么type
表示被引用的類型。我們重寫一個函數以返回元素值的拷貝而不是引用:
template <typename It>
auto fcn2(It beg, It end) ->
typename remove_reference<decltype(*beg)>::type
{
// 處理序列
return *beg; // 返回序列中一個元素的拷貝
}
對Mod<T> ,其中Mod 為 |
若T 為 |
則Mod<T>::type 為 |
---|---|---|
remove_reference |
X& 或X&& <br />否則 |
X <br />T
|
add_const |
X& 、const X 或函數<br />否則 |
T <br />const T
|
add_lvalue_reference |
X& <br />X&& <br />否則 |
T <br />X& <br />T&
|
add_rvalue_reference |
X& 或X&& <br />否則 |
T <br />T&&
|
remove_pointer |
X* <br />否則 |
X <br />T
|
add_pointer |
X& 或X&& <br />否則 |
X* <br />T*
|
make_signed |
unsigned X <br />否則 |
X <br />T
|
make_unsigned | 帶符號類型<br />否則 |
unsigned X <br />T |
remove_extent |
X[n] <br />否則 |
X <br />T
|
remove_all_extents |
X[n1][n2]... <br />否則 |
X <br />T
|
4.函數指針和實參推斷
當我們用一個函數模板初始化一個函數指針或者為一個函數指針賦值時,編譯器使用指針的類型來推斷模板實參。
template <typename T> int compare(const T&, const T&);
// pf1指向實例int compare(const int&, const int&)
int (*pf1)(const int&, const int&) = compare;
如果不能從函數指針類型確定模板實參,那么會產生錯誤:
// func的重載版本: 每個版本接受一個不同的函數指針類型
void func(int(*)(const string&, const string&));
void func(int(*)(const int&, const int&));
func(compare); // 錯誤: 使用compare哪個實例?
// 我們可以通過顯式模板實參來消除func調用的歧義
func(compare<int>); // 傳遞compare(const int&, const int&)
5.模板實參推斷和引用
為了理解如何從函數調用進行類型推斷,考慮下面例子:
template <typename T> void f(T &p);
其中函數參數p
是一個模板類型參數T
的引用,需要記住非常重要的兩點:
- 編譯器會應用正常的引用綁定規則
-
const
是底層的,不是頂層的
5.1 從左值引用函數參數推斷類型
當一個函數參數是模板類型參數的一個普通(左值)引用時(即形如T&
),綁定規則告訴我們只能給它一個左值(比如一個變量或者一個返回引用類型的表達式)。實參可以是const
類型也可以不是。如果實參是const
的,那么T
將會被推斷為const
:
template <typename T> void f1(T&); // 實參必須是一個左值
f1(i); // i是一個int; 模板參數類型T是int
f1(ci); // ci是一個const int; 模板參數T是const int
f1(5); // 錯誤: 傳遞給一個&參數的實參必須是一個左值
如果一個函數參數的類型是const T&
,正常的綁定規則告訴我們可以傳遞給它任何類型的實參——一個對象(const
或者非const
)、一個臨時對象或是一個字面常量值。當函數參數本身是const
時,T
的類型推斷的結果不會是一個const
,const
已經是函數參數類型的一部分,因此它不會也是模板參數類型的一部分:
template <typename T> void f2(const T&); // 可以接受一個右值
// f2中的參數是const &; 實參中的const是無關的
// 在每個調用中, f2的函數參數都被推斷為const int&
f2(i); // i是一個int;模板參數T是int
f2(ci); // ci是一個const int, 但模板參數T是int
f2(5); // 一個const &參數可以綁定到一個右值; T是int
5.2 從右值引用函數參數推斷類型
當一個函數參數是一個右值引用(即形如T&&
),正常綁定規則告訴我們可以傳遞給它一個右值:
template <typename T> void f3(T&&);
f3(42); // 實參是一個int類型的右值; 模板參數T是int
5.3 引用折疊和右值引用參數
假定i
是一個int
對象,我們可能認為像f3(i)
這樣的調用是不合法的。畢竟i
是一個左值,而通常我們不能將一個右值引用綁定到一個左值上。但是C++
在正常綁定規則外定義了兩個例外規則,允許這種綁定:
- 第一個例外規則:當我們將一個左值(如
i
)傳遞給函數的右值引用參數,且此右值引用指向模板類型參數(如T&&
)時,編譯器推斷模板類型參數為實參的左值引用類型。因此,當我們調用f3(i))
時,編譯器推斷T
的類型為int&
而非int
通常我們不能(直接)定義一個引用的引用,但是通過類型別名或通過模板類型參數間接定義是可以的
- 第二個例外規則:如果我們間接創建一個引用的引用,則這些引用形成了“折疊”,在所有情況下(除了一個例外),引用會折疊成一個普通的左值引用類型。只有一種情況下回折疊成右值引用:右值引用的右值引用。
對于一個給定類型X
:
-
X& &
、X& &&
和X&& &
都折疊成X&
-
X&& &&
折疊成X&&
這兩個規則導致了兩個重要結果:
- 如果一個函數參數是一個指向模板類型參數的右值引用(即如
T&&
),則它可以被綁定到一個左值 - 如果實參是一個左值,則推斷出模板實參類型將是一個左值引用,且函數參數將被實例化為一個(普通)左值引用參數(
T&
)
這兩個規則暗示我們將任意類型的實參傳遞給
T&&
類型的函數參數,對于這種類型的參數,(顯然)可以傳遞給它右值,也可以傳遞給它左值。
5.4 編寫接收右值引用參數的模板函數
如果一個函數參數是一個指向模板類型參數的右值引用(即如T&&
),模板內的代碼就會產生歧義:
template <typename T> void f3(T&& val)
{
T t = val; // 實參是左值時, 模板參數T是int&, 那么是綁定一個引用; 實參是右值時, 模板參數T是int, 那么是拷貝val值到t
t = fcn(t); // 賦值是只改變t還是既改變t又改變val
if (val == t) { /*...*/ } // 如果T是引用類型, 則一直為true
}
如上所述,當代碼中涉及的類型可能是普通(非引用)類型,也可能是引用類型時,編寫正確的代碼就變得異常困難(雖然remove_reference
這樣的類型轉換可能有所有幫助)。
在實際中,右值引用通常用于兩種情況:模板轉發其實參或模板被重載。使用右值引用的函數模板通常要進行重載:
// 右值會調用第一個函數, 排除歧義的問題
template <typename T> void f(T&&); // 綁定到非const右值
template <typename T> void f(const T&); // 左值和cosnt右值
6. 理解std::move
雖然不能直接將一個右值引用綁定到一個左值上,但可以用move
獲得一個綁定到左值上的右值引用。
6.1 std::move在標準庫中的定義
// 在返回類型和類型轉換中也要用到typename
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type&&>(t);
}
move
的函數參數T&&
是一個指向模板類型參數的右值引用,通過引用折疊意味著該參數可以與任何類型的實參匹配。特別是我們既可以給move
傳遞一個左值,又可以給它傳遞一個右值:
string s1("hi!"), s2;
s2 = std::move(string("bye!")); // 正確: 從一個右值移動數據
s2 = std::move(s1); // 正確: 但是賦值之后, s1的值是不確定的
6.2 std::move是如何工作的
仍然看上面的例子:
s2 = std::move(string("bye!"));
- 推斷出
T
的類型是string
-
remove_reference
用string
實例化 -
remove_reference
的type
成員是string
-
move
返回類型是string &&
-
move
的函數參數t
的類型為string&&
因此這個調用實例化即函數:
string&& move(string &&t)
// 參數t的類型已經是string&&, 因此函數體類型轉換什么都不做, 返回它所接受的右值引用
看第二個例子:
s2 = std::move(s1);
- 推斷出
T
的類型是string &
-
remove_reference
用string&
實例化 -
remove_reference<string&>
的type
成員是string
-
move
返回類型string &&
-
move
的函數參數t
實例化為string& &&
,會折疊成string &
因此這個調用實例化即:
string&& move(string &t)
6.3 從一個左值static_cast
到一個右值引用是允許的
- 雖然不能隱式地將一個左值轉換成右值引用,但是我們可以用
static_cast
顯式轉換 - 使用
static_cast
顯式將一個左值轉換成右值引用,會截斷一個左值,只有當你確保截斷后是安全的才這么操作 - 使用
std::move
使我們在程序 中查找潛在的截斷左值的代碼很容易
7. 轉發
某些函數需要將其一個或多個實參聯通類型不變地轉發給其他參數,這種情況我們需要保持被轉發實參的所有性質:
- 實參類型是不是
const
- 實參是左值還是右值
看一下這個例子,我們編寫一個函數接受一個可調用表達式和兩個額外實參:
// 對"翻轉"的參數調用給定的可調用對相關
// flip1是一個不完整的實現: 頂層const和引用丟失了
template <typename F, typename T1, typename T2>
void flip1(F f, T1 t1, T2 t2)
{
f(t2, t1);
}
這個函數一般工作得很好,但當我們希望用它調用一個接受引用參數的函數就會出現問題:
void f(int v1, int &v2) // v2是一個引用
{
cout << v1 << " " << ++v2 << endl;
}
當我們通過flip1
調用f
時就會丟失v2
的引用信息。
我們可以使用forward
的新標準庫來傳遞flip2
的參數,它能保持原始參數的類型。與move
不同的是,forward
必須通過顯式模板實參來調用,forward
返回該顯式實參類型的右值引用。即forward<T>
的返回類型是T&&
。通過返回類型上的引用折疊,forward
可以保持給定實參的左值/右值屬性。
template <typename Type> intermediary(Type &&arg)
{
finalFcn(std::forward<Type>(arg));
// ...
}
我們使用Type
作為forward
的顯式模板實參類型,它是從arg
推斷出來的。由于arg
是一個模板類型參數的右值引用,Type
將表示傳遞給arg
的實參的所有類型信息:
- 如果實參是一個右值,那么
Type
是一個普通(非引用)類型,forward<Type>
將返回Type&&
- 如果實參是一個左值,那么通過引用折疊,
Type
本身是一個左值引用類型,forward
返回類型是一個指向左值引用類型的右值引用,折疊后返回一個左值引用類型
當用于一個指向模板參數類型的右值引用函數參數T&&
時,forward
會保持實參類型的所有細節。使用forward
,我們可以再次重寫反轉函數:
template <typename F, typename T1, typename T2>
void flip(F f, T1 &&t1, T2 &&t2)
{
f(std::forward<T2>(t2), std::forward<T1>(t1));
}
重載與模板
函數模板可以被另一個模板或一個普通非模板函數重載。與之前一樣,名字相同的函數必須具有不同數量或類型的參數。
1. 編寫重載模板
我們構造一組調試函數命名為debug_rep
,每個函數返回一個給定對象的string
表示。我們首先編寫此函數的最通用版本,并將它定義為接受一個const
對象引用的模板:
template <typename T> string debug_rep(const T &t)
{
ostringstream ret;
ret << t; // 使用T的輸出運算符打印t的一個表示形式
return ret.str(); // 返回ret綁定的string的一個副本
}
再定義打印指針的版本:
// 注意此函數不能用于char*對象, 因為IO庫為char*定義了一個<<版本, 此版本假定指針表示一個空字符結尾的字符數組, 并打印數組的內容而非地址
template <typename T> string debug_rep(T *p)
{
ostringstream ret;
ret << "pointer: " << p; // 打印地址值
if (p)
ret << " " << debug_rep(*p); // 打印p指向的值
else
ret << " null pointer"; // 指出p為空
return ret.str();
}
使用:
// 只有第一個版本是可行的, 因為編譯器無法從一個費指針參數實例化一個期望值真類型參數的函數模板
string s("hi");
cout << debug_rep(s) << endl;
// 如果用指針調用, 則兩個版本都是可行的
cout << debug_rep(&s) << endl;
// 第一個版本的T被綁定到string*, 實例化debug_rep(const string*&)
// 第二個版本的T被綁定到string, 實例化debug_rep(string*)
第一個版本的實例需要進行普通指針到
const
指針的轉換,正常函數匹配規則告訴我們應該選擇第二個版本。
2. 多個可行模板
當多個重載模板對一個調用提供同樣好的匹配時,應選擇最特例化的版本。
3. 非模板和模板重載
對于一個調用,如果一個非函數模板與一個函數模板提供同樣好的匹配,則選擇非模板版本。
4. 重載模板與類型轉換
考慮一下C
風格字符串指針和字符串字面常量。考慮一下這個調用:
cout << debug_rep("hi world!") << endl; // 調用debug_rep(T*)
有三個debug_rep
版本都是可行的:
-
debug_rep(const T&)
,T
被綁定到char[10]
-
debug_rep(T*)
,T
被綁定到const char
-
debug_rep(const sring&)
,要求從const char*
到string
的類型轉換
對給定實參來說,兩個模板都提供精確匹配(第二個模板需要進行一次數組到指針的轉換,對于函數匹配來說,這種轉換被認為是精確匹配)。非模板版本是可行的,但是需要進行一次用戶定義的類型轉換,因此沒有精確匹配那么好,所以兩個模板稱為可能被調用的函數。由于第二個模板T*
更加特例化,因此編譯器會選擇它。
5. 缺少聲明可能導致程序行為異常
通常如果使用了一個忘記聲明的函數,代碼將編譯失敗。但是對于重載函數模板的函數而言,如果編譯器可以從模板實例化出與調用匹配的版本,則缺少的聲明就不會報錯。以前面的例子而言,如果缺少了接收T*
的模板版本,則編譯器會默認實例化接受const T&
的模板版本。
在定義任何函數之前,記得聲明所有重載的函數版本。這樣就不必擔心編譯器由于未遇到你希望調用的函數而實例化一個并非你需要的版本。
可變參數模板
一個可變參數模板variadic template
就是一個接受可變數組參數的模板函數或模板類。可變數目的參數被稱為參數包parameter packet
,參數包包括模板參數包和函數參數包。
// Args是一個模板參數包; rest是一個函數參數包
// Args表示零個或多個模板類型參數
// rest表示零個或多個函數參數
template <typename T, typename... Args>
void foo(const T &t, const Args& ... rest)
// 調用方式
int i = 0; double d = 3.14; string s = "how now brown cow";
foo(i, s, 42, d); // 包中三個參數
foo(s, 42, "hi"); // 包中兩個參數
foo(d, s); // 包中一個參數
foo("hi"); // 空包
// 編譯器會分別實例化對應的版本
void foo(const int&, const string&, const int&, const double&);
void foo(const string&, const int&, const char[3]&);
void foo(const double&, const string&);
void foo(const char[3]&);
1. sizeof...運算符
當我們需要知道包中有多少元素時,可以使用sizeof...
運算符:
template <typename ... Args> void g(Args ... args) {
cout << sizeof...(Args) << endl; // 類型參數的數目
cout << sizeof...(args) << endl; // 函數參數的數目
}
2. 編寫可變參數函數模板
// 用于終止遞歸并打印最后一個元素的函數
// 此函數必須在可變參數版本的print定義之前聲明
template <typename T>
ostream &print(ostream &os, const T &t)
{
return os << t; // 包中最后一個元素之后不打印分隔符
}
// 包中除了最后一個元素之外的其他元素都會調用這個版本的print
template <typename T, typename... Args>
ostream &print(ostream &os, const T &t, const Args&... rest)
{
os << t << ", "; // 打印第一個實參
return print(os, rset...); // 遞歸調用, 打印其他實參
}
// 調用:
print(cout, i, s, 42); // 包中有兩個參數
// 依次執行:
print(cout, i, s, 42); // t:i, rset:s, 42
print(cout, s, 42); // t:s, rset:42
print(cout, 42); // 調用非可變參數版本的print
- 對于最后一個調用,兩個函數提供同樣好的匹配,但是非可變參數模板比可變參數模板更加特例化,因此編譯器選擇非可變參數版本
- 當定義可變參數版本的
print
時,非可變參數版本的聲明必須在作用域中,否則可變參數版本會無限遞歸
3. 包擴展
我們前面提到的print
函數包含兩個擴展:
template <typename T, typename... Args>
ostream &print(ostream &os, const T &t, const Args&... rest) // 擴展Args
{
os << t << ", ";
return print(os, rset...); // 擴展rest
}
C++
還支持更復雜的擴展模式,我們可以編寫第二個可變參數函數,對其每個實參調用debug_rep
,然后調用print
打印結果的string
:
// 在print調用中對每個實參調用debug_rep
template <typename... Args>
ostream &errorMsg(ostream &os, const Args&... rest)
{
// 等價于print(os, debug_rep(a1), debug_rep(a2), ..., debug_rep(an))
return print(os, debug_rep(rset)...);
}
4. 轉發參數包
可變參數函數通常將它們的參數轉發給其他函數,這種函數通常與我們的emplace_back
函數具有一樣的形式:
// fun有零個或多個參數, 每個參數都是一個模板參數類型的右值引用
template<typename... Args>
void fun(Args&&... args) // 將Args擴展為一個右值引用的列表
{
// work的實參既擴展Args又擴展args
work(std::forward<Args>(args)...);
}
我們希望將fun
的所有實參轉發給另一個名為work
的函數,假定它完成函數的實際工作。類似于emplace_back
中對construct
的調用,work
調用中的擴展既擴展了模板參數包也擴展了函數參數包。由于fun
的參數是右值引用,因此我們可以傳遞給它任意類型的實參,由于我們使用std::forward
傳遞這些實參,因此它們的所有信息類型在調用work
時都會得到保持。
模板特例化
繼續看我們之前定義的compare
函數:
// 第一個版本: 可以比較任意兩個類型
template <typename T> int compare(const T&, const T&);
// 第二個版本處理字符串字面常量
template<size_t N, size_t M>
int compare(const char (&)[N], const char (&)[M]);
只有當我們傳遞給compare
一個字符串字面常量或者一個數組時,編譯器才會調用第二個版本,如果我們傳遞給它字符指針,就會調用第一個版本(我們無法將一個指針轉換為一個數組的引用):
const char *p1 = "hi", *p2 = "mom";
compare(p1, p2); // 調用第一個模板版本
compare("hi", "mom"); // 調用第二個版本
1. 定義函數模板特例化
為了處理字符指針(而不是數組),可以為第一個版本的compare
定義一個模板特例化版本。
模板特例化版本就是模板的一個獨立的定義,在其中一個或多個模板參數被指定為特定的類型。
// compare的特殊版本, 處理字符數組的指針
template<>
int compare(const char* const&p1, const char* const &p2)
{
return strcmp(p1, p2);
}
一個特例化的版本本質上是一個實例,而非函數名的一個重載版本。因此特例化不影響函數匹配。另外需要注意的是:
- 為了特例化一個模板,原模板的聲明必須在作用域之中;在任何使用模板實例的代碼之前,特例化版本的聲明也必須在作用域中
- 從前一條得知:模板及其特例化版本應該聲明在同一個頭文件中,所有同名模板的聲明應該放在前面,然后是這些模板的特例化版本
2. 類模板特例化
除了特例化函數模板,我們還可以特例化類模板。唯一需要注意的是,我們必須在原模板定義所在的命名空間中特例化它。
3. 類模板部分特例化
與函數模板不同,類模板的特例化不必為所有模板參數提供實參。我們可以只指定一部分而非所有模板參數,或是參數的一部分而非全部特性。一個類模板的部分特例化本身是一個模板,使用它時用戶還必須為那些在特例化版本中未指出的模板參數提供實參:
// 原始的通用的版本
typename <class T> struct remove_reference {
typedef T type;
};
// 部分特例化版本, 將用于左值引用和右值引用
template <class T> struct remove_reference<T&> // 左值引用
{ typedef T type; };
template <class T> struct remove_reference<T&&> // 右值引用
{ typedef T type; };
// 調用
int i;
// decltype(42)為int, 使用通用版本
remove_reference<decltype(42)>::type a;
// decltype(i)為int&, 使用第一個版本T&
remove_reference<decltype(i)>::type b;
// decltype(std::move(i))為int&&, 使用第二個版本T&&
remove_reference<decltype(std::move(i))>::type c;
4. 特例化成員而不是類
假定Foo
是一個模板類,包含一個成員Bar
,我們可以只特例化該成員:
template <typename T> struct Foo {
Foo(const T &t = T()) : mem(t) { }
void Bar() { /*...*/ }
T mem;
// Foo其他成員
};
template<> // 我們正在特例化一個模板
void Foo<int>::Bar() // 我們正在特例化Foo<int>的成員Bar