《C++Primer》第十六章 模板與泛型編程

第十六章 模板與泛型編程

定義模板

1. 函數(shù)模板

模板定義以關(guān)鍵字template關(guān)鍵字開始千诬,后面跟著一個(gè)模板參數(shù)列表(不能為空):

template <typename T>
int compare(const T &v1, const T &v2)
{
    if (v1 < v2) return -1;
    if (v2 < v1) return 1;
    return 0;
}

類型參數(shù)可以用來指定返回類型或函數(shù)的參數(shù)類型,以及在函數(shù)體內(nèi)用于變量聲明或類型轉(zhuǎn)換:

template <typename T> T foo(T* p) {
    T tmp = *p;  // tmp的類型是指針p指向的類型
    // ...
    return tmp;
}
1.1 非類型模板參數(shù)

除了定義類型參數(shù)膏斤,還可以定義非類型參數(shù)nontype parameter徐绑,一個(gè)非類型參數(shù)表示一個(gè)值而非一個(gè)類型。當(dāng)一個(gè)模板被實(shí)例化時(shí)掸绞,非類型參數(shù)被一個(gè)用戶提供的或者編譯器推斷出來的值鎖替代泵三,這些紙必須是常量表達(dá)式,從而允許編譯器在編譯時(shí)實(shí)例化模板衔掸。

比如我們編寫一個(gè)compare版本處理字符串字面常量(const char的數(shù)組)烫幕,由于不能拷貝數(shù)組,因此我們將自己的參數(shù)定義為數(shù)組的引用敞映,由于我們希望能夠比較不同長(zhǎng)度的字符串字面常量较曼,因此為模板定義了兩個(gè)非類型的參數(shù):第一個(gè)模板參數(shù)表示第一個(gè)數(shù)組長(zhǎng)度,第二個(gè)參數(shù)表示第二個(gè)數(shù)組的長(zhǎng)度:

template<unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M])
{
    return strcmp(p1, p2);
}

// 調(diào)用
compare("hi", "mom");
// 編譯器相當(dāng)于實(shí)例化如下版本:
int compare(const char (&p1)[3], const char(&p2)[4])
  • 非類型參數(shù)可以是一個(gè)整型振愿,或者是一個(gè)指向?qū)ο蠡蛘吆瘮?shù)類型的指針或(左值)引用
  • 綁定到非類型參數(shù)的實(shí)參必須是一個(gè)常量表達(dá)式
  • 綁定到指針或者引用非類型參數(shù)的實(shí)參必須具有靜態(tài)的生存期
1.2 編寫類型無(wú)關(guān)的代碼

編寫泛型代碼的兩個(gè)重要原則:

  • 模板中的函數(shù)參數(shù)是const的引用
  • 函數(shù)體中的條件判斷僅使用<比較運(yùn)算

通過將函數(shù)參數(shù)設(shè)定為const引用捷犹,我們保證函數(shù)可以用于不能拷貝的類型。大多數(shù)類型(除了unique_ptrIO類型外)都是允許拷貝的冕末,但是不允許拷貝的類型也是存在的萍歉,而且使用常量引用在處理大對(duì)象時(shí)也可以使函數(shù)運(yùn)行地更快。

1.3 模板編譯

當(dāng)編譯器遇到一個(gè)模板定義時(shí)档桃,它并不生成代碼枪孩。只有當(dāng)我們實(shí)例化出模板的一個(gè)特定版本時(shí),編譯器才會(huì)生成代碼藻肄。即當(dāng)我們使用而非定義模板時(shí)蔑舞,編譯器才生成代碼

通常當(dāng)我們調(diào)用一個(gè)函數(shù)時(shí)嘹屯,編譯器只需要掌握函數(shù)的聲明攻询。類似地,當(dāng)我們使用一個(gè)類類型的對(duì)象時(shí)州弟,類定義必須是可用的钧栖,但是成員函數(shù)的定義不必已經(jīng)出現(xiàn)。因此我們通常將類定義和函數(shù)聲明放在頭文件中婆翔,而普通函數(shù)和類的成員函數(shù)的定義放在源文件中桐经。

為了生成一個(gè)實(shí)例化版本,編譯器需要掌握函數(shù)模板或者類模板成員函數(shù)的定義浙滤。因此與非模板文件不同阴挣,模板的頭文件通常既包含聲明也包含定義。

模板直到實(shí)例化時(shí)才會(huì)生成代碼纺腊,這一特性影響了我們何時(shí)才會(huì)直到模板內(nèi)代碼的編譯錯(cuò)誤:

  • 第一個(gè)階段:編譯模板本身畔咧。編譯器只能檢查語(yǔ)法錯(cuò)誤茎芭,比如忘記分號(hào)或者變量名拼錯(cuò)。
  • 第二個(gè)階段:編譯器遇到模板使用時(shí)誓沸。對(duì)于函數(shù)模板調(diào)用梅桩,編譯器通常會(huì)檢查實(shí)參數(shù)目是否正確,還能檢查參數(shù)類型是否匹配拜隧;對(duì)于類模板宿百,編譯器可以檢查用戶是否提供了正確數(shù)目的模板實(shí)參
  • 第三個(gè)階段:模板實(shí)例化。只有這個(gè)階段才能發(fā)現(xiàn)類型相關(guān)的錯(cuò)誤洪添,這類錯(cuò)誤可能在鏈接時(shí)才報(bào)告垦页。

2. 類模板

類模板class template使用來生成類的藍(lán)圖的。

2.1 定義類模板
template <typename T> class Blob {
public:
    typedef T value_type;
    typedef typename std::vector<T>::size_type size_type;
    // 構(gòu)造函數(shù)
    Blob();
    Blob(std::initializer_list<T> il);
    // Blob中的元素?cái)?shù)目
    size_type size() const { return data->size(); }
    bool empty() const { return data->empty(); }
    // 添加和刪除元素
    void push_back(const T &t) { data->push_back(t); }
    // 移動(dòng)版本
    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]無(wú)效干奢,則拋出msg
    void check(size_type i, const std::string &msg) const;
};
2.2 實(shí)例化類模板

當(dāng)使用一個(gè)類模板時(shí)痊焊,我們必須提供額外信息,即顯式模板實(shí)參explicit template argument忿峻,編譯器使用這些模板實(shí)參來實(shí)例化出特定的類薄啥。

Bolb<int> ia;
Blob<int> ia2 = {0,1,2,3,4};
2.3 在模板作用域內(nèi)引用模板類型

舉個(gè)例子,我們的data成員使用了兩個(gè)模板:vectorshared_ptr逛尚,我們將其定義為:

std::shared_ptr<std::vector<T>> data;
2.4 類模板的成員函數(shù)

Blob的成員函數(shù)應(yīng)該這么定義:

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構(gòu)造函數(shù)
template <typename T>
// 構(gòu)造函數(shù)分配一個(gè)空vector, 并將指向vector的指針保存在data中
Bolb<T>::Blob() : data(std::make_shared<std::vector<T>>()) { }

// 接受一個(gè)initializer_list參數(shù)的構(gòu)造函數(shù)將其類型參數(shù)為T作為initializer_list參數(shù)的元素類型
template <typename T>
Blob<T>::Blob(std::initializer_list<T> il) : data(std::make_shared<std::vector<T>>(il)) { }

// 使用方法: 在這條語(yǔ)句中構(gòu)造函數(shù)的參數(shù)類型為initializer_list<string>,列表中每個(gè)字符串字面常量會(huì)被隱式地轉(zhuǎn)換為一個(gè)string
Blob<string> articles = {"a", "an", "the"};
2.6 類模板成員函數(shù)的實(shí)例化

默認(rèn)情況下一個(gè)類模板的成員函數(shù)只有當(dāng)程序用到它時(shí)才進(jìn)行實(shí)例化垄惧,成員函數(shù)只有被用到時(shí)才進(jìn)行實(shí)例化,這一特性使得即使某種類型不能完全符合模板操作的要求绰寞,我們?nèi)匀荒苡迷擃愋蛯?shí)例化類到逊。

2.7 在類模板的作用域內(nèi)簡(jiǎn)化模板類名的使用
// 后置: 遞增/遞減對(duì)象但返回原值
template <typenname T>
BlobPtr<T> BlobPtr<T>::operator++(int)
{
    // 此處無(wú)須檢查, 調(diào)用前置遞增時(shí)會(huì)進(jìn)行檢查
    BlobPtr ret = *this; // 保存當(dāng)前值, 這里等價(jià)于BlobPtr<T> ret = *this; 在類模板作用域內(nèi)簡(jiǎn)化模板類名的使用
    ++ *this;    // 推進(jìn)一個(gè)元素, 前置++檢查遞增是否合法
    return ret;  // 返回保存的狀態(tài)
}
2.8 類模板和友元

類模板和另一個(gè)(類或者函數(shù))模板間友好關(guān)系最常見的形式是建立對(duì)應(yīng)實(shí)例及其友元間的友好關(guān)系:

// 首先將Blob、BlobPtr和operator==聲明為模板, 這些聲明是operator==函數(shù)的參數(shù)聲明及Blob中的友元聲明所需要的
template <typename> class BlobPtr;
template <typename> class Blob; // 運(yùn)算符==中的參數(shù)所需要的
template <typename T>
    bool operator==(const Blob<T>&, const Blob<T>&);

template <typename T> class Blob {
    // 每個(gè)Blob實(shí)例將訪問權(quán)限授予相同類型實(shí)例化的BlobPtr和相等運(yùn)算符
    friend class BlobPtr<T>;
    friend bool operator==<T>
        (const Blob<T>&, const Blob<T>&);
}

// 友元的聲明用Blob的模板形參作為他們自己的模板形參克握,因此友好關(guān)系被限定在用相同類型實(shí)例化的Blob與BlobPtr相等運(yùn)算符之間
Blob<char> ca;   // BlobPtr<char>和operator==<char>都是本對(duì)象的友元
Blob<int> ia;    // BlobPtr<int>和operator==<int>都是本對(duì)象的友元
// BlobPtr<char>的成員可以訪問ca(或者任意其他Blob<char>對(duì)象)的非public部分, 但對(duì)ia或任何其他Blob<int>對(duì)象或Blob的其他實(shí)例都沒有特殊訪問權(quán)限

一個(gè)類也可以將另一個(gè)模板的每個(gè)實(shí)例都聲明為自己的友元,或者限定特定的實(shí)例為友元:

// 前置聲明, 在將模板的一個(gè)特定實(shí)例聲明為友元時(shí)將要用到
template <typename t> class Pal;
class C { // C時(shí)一個(gè)普通的非模板類
    friend class Pal<C>;   // 用類C實(shí)例化的Pal是C的一個(gè)友元
    // Pal2的所有實(shí)例都是C的友元, 這種情況無(wú)須前置聲明
    template <typename T> friend class Pal2;
};

template <typename T> class C2 { // C2本身是一個(gè)類模板
    // C2的每個(gè)實(shí)例將相同實(shí)例化的Pal聲明Pal聲明成友元
    friend class Pal<T>;  // Pal的模板聲明必須在作用域之內(nèi)
    // Pal2的所有實(shí)例都是C2的每個(gè)實(shí)例的友元枷踏,不需要前置聲明
    template <typename X> friend class Pal2;
    // Pal3是一個(gè)非模板類菩暗,它是C2所有實(shí)例的友元
    friend class Pal3;
}

在新標(biāo)準(zhǔn)中,我們可以將模板類型參數(shù)聲明為友元旭蠕,這樣對(duì)于某個(gè)類型名Foo停团,Foo將成為Bar<Foo>的友元:

template <typename Type> class Bar {
friend Type; // 將訪問權(quán)限授予用來實(shí)例化Bar的類型
    // ...
}
2.9 模板類型別名

我們可以通過定義一個(gè)typedef來引用實(shí)例化后的類:

typedef Blob<string> StrBlob;d

由于模板并不是一個(gè)類,因此我們不能定義一個(gè)typedef引用Blob<T>掏熬,但是新標(biāo)準(zhǔn)允許我們?yōu)轭惸0宥x一個(gè)類型別名:

template <typename T> using twin = pair<T, T>;
twin<string> authors;  // authors是一個(gè)pair<string, string>

// 我們也可以固定一個(gè)或者多個(gè)模板參數(shù)
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;
};

每個(gè)Foo的實(shí)例都有其自己的static成員實(shí)例佑稠,即給定任意類型X喉前,都有一個(gè)Foo<X>::ctrFoo<X>::count成員疑故,所有的Foo<X>類型的對(duì)象共享相同的ctr對(duì)象和count函數(shù)。由于類的每個(gè)實(shí)例都有一個(gè)獨(dú)有的static對(duì)象答倡,因此我們可以將它的static成員也定義成模板:

template <typename T>
size_t Foo<T>::ctr = 0;

3. 模板參數(shù)

3.1 模板聲明

一個(gè)特定文件所需要的所有模板的聲明通常一起放置在文件開始位置疮丛,出現(xiàn)于任何使用這些模板的代碼之前幔嫂,原因我們將在后面講辆它。

3.2 使用類的類型成員

我們用作用域運(yùn)算符::來訪問static成員和類型成員,在普通(非模板)代碼中履恩,編譯器掌握類的定義锰茉,因此它直到通過作用域運(yùn)算符訪問的名字是類型還是static成員。比如當(dāng)我們寫下string::size_type切心,由于編譯器有string的定義飒筑,因此它知道size_type是一個(gè)類型。

對(duì)于模板代碼來說就不是這么簡(jiǎn)單绽昏,假定T是一個(gè)模板類型參數(shù)协屡,當(dāng)編譯器遇到T::mem代碼時(shí),它在實(shí)例化之前不知道mem是一個(gè)類型成員還是一個(gè)static數(shù)據(jù)成員而涉。比如編譯器遇到如下語(yǔ)句時(shí):

T::size_type *p
// 編譯器需要知道我們是正在定義一個(gè)名為`p`的變量著瓶,還是將一個(gè)名為size_type的static數(shù)據(jù)成員與名為p的變量相乘

如果我們希望使用一個(gè)模板類型參數(shù)的類型成員,就必須顯式告訴編譯器該名字是一個(gè)類型啼县,我們通過使用關(guān)鍵字typename來實(shí)現(xiàn)這一點(diǎn):

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 默認(rèn)模板實(shí)參

在新標(biāo)準(zhǔn)中我們可以為函數(shù)和類模板提供默認(rèn)實(shí)參:

// compare有一個(gè)默認(rèn)模板實(shí)參less<T>和一個(gè)默認(rèn)函數(shù)實(shí)參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;
}

與函數(shù)默認(rèn)實(shí)參一樣材原,對(duì)于一個(gè)模板參數(shù),只有它右側(cè)的所有參數(shù)都有默認(rèn)實(shí)參時(shí)季眷,它才可以有默認(rèn)實(shí)參余蟹。

3.4 模板默認(rèn)實(shí)參與類模板

如果一個(gè)類模板為其所有模板參數(shù)都提供了默認(rèn)實(shí)參,且我們希望使用這些默認(rèn)實(shí)參子刮,就必須在模板名后面加上一個(gè)空尖括號(hào)對(duì):

template <class T = int> class Numbers { // T 默認(rèn)為int
public:
    Numbers(T v = 0) : val { }
    // 其他操作
private:
    T val;
};

Number<long double> lots_of_precision;
Number<> average_precision; // 空<>表示我們希望使用默認(rèn)類型

4. 成員模板

一個(gè)類可以包含本身是模板的成員函數(shù)威酒,這種成員被稱為成員模板,成員模板不能是虛函數(shù)挺峡。

4.1 普通(非模板)類的成員模板

我們定義一個(gè)類葵孤,類似于unique_ptr所使用的默認(rèn)刪除器類型。我們的類將包含一個(gè)重載的函數(shù)調(diào)用運(yùn)算符橱赠,它接受一個(gè)指針并對(duì)此指針執(zhí)行delete尤仍,由于我們希望刪除器適用于任何類型,因此我們將調(diào)用運(yùn)算符定義為一個(gè)模板狭姨。

// 函數(shù)對(duì)象類. 對(duì)給定指針執(zhí)行delete
class DebugDelete {
public:
    DebugDelete(std::ostream &s = std::cerr) : os(s) { }
    // 與任何函數(shù)模板相同, T的類型由編譯器推斷
    template <typename T> void operator()(T *p) const
        { os << "deleting unique_ptr" << std::endl; delete p; }
private:
    std::ostream &os;
}

我們可以使用這個(gè)類代替delete函數(shù):

double* p = new double;
DebugDelete d; // 可像delete表達(dá)式一樣使用的對(duì)象
d(p); // 調(diào)用DebugDelete::operator()(double*), 釋放p
int* ip = new int;
// 在一個(gè)臨時(shí)DebugDelete對(duì)象上調(diào)用operator()(int*)
DebugDelete()(ip);

我們可以將DebugDelete用作unique_ptr的刪除器:

// 刪除p指向的對(duì)象
// 實(shí)例化DebugDelete::operator()<int>(int *)
unique_ptr<int, DebugDelete> p(new int, DebugDelete());
//銷毀sp執(zhí)行的對(duì)象
unique_ptr<string, DebugDelete> sp(new string, DebugDelete());
4.2 類模板的成員模板

對(duì)于類模板宰啦,我們也可以定義它的成員模板,這種情況下饼拍,類和成員各自有自己的赡模、獨(dú)立的模板參數(shù)。

template <typename T> class Blob {
    // 構(gòu)造函數(shù)是一個(gè)成員模板, 接受兩個(gè)迭代器, 表示要拷貝的元素范圍
    template <typename It> Blob(It b, It e);
    // ...
};

// 當(dāng)我們?cè)陬惸0逋舛x一個(gè)成員模板時(shí), 必須同時(shí)為類模板和成員模板提供模板參數(shù)列表, 類模板的參數(shù)列表在前, 后跟成員的模板參數(shù)列表
template <typename T>  // 類的類型參數(shù)
template <typename It> // 構(gòu)造函數(shù)的類型參數(shù)
    Blob<T>::Blob(It b, It e) : data(std::make_shared<std::vector<T>>(b, e)) { }
4.3 實(shí)例化與成員模板

為了實(shí)例化一個(gè)類模板的成員模板师抄,我們必須同時(shí)提供類和函數(shù)模板的實(shí)參漓柑。與普通函數(shù)模板相同,編譯器通常根據(jù)傳遞給成員模板的函數(shù)實(shí)參來推斷它的模板實(shí)參:

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"};
// 實(shí)例化Blob<int>類及其接受兩個(gè)int*參數(shù)的構(gòu)造函數(shù)
Blob<int> a1(begin(ia), end(ia));
// 實(shí)例化Blob<int>類及其接受兩個(gè)vector<long>::iterator的構(gòu)造函數(shù)
Blob<int> a2(vi.begin(), vi.end());
// 實(shí)例化Blob<string>及其接受兩個(gè)list<const char*>::iterator參數(shù)的構(gòu)造函數(shù)
Blob<string> a3(w.begin(), w.end());

5. 控制實(shí)例化

前面我們提到只有當(dāng)模板被使用時(shí)才會(huì)進(jìn)行實(shí)例化,這一特性意味著相同的實(shí)例可能出現(xiàn)在多個(gè)對(duì)象文件中欺缘。

當(dāng)多個(gè)獨(dú)立編譯的源文件使用了相同的模板栋豫,并且提供了相同的模板參數(shù)時(shí),每個(gè)文件中就都會(huì)有該模板的一個(gè)實(shí)例谚殊。在大系統(tǒng)中丧鸯,如果我們?cè)诙鄠€(gè)文件中實(shí)例化相同模板的額外開銷可能非常嚴(yán)重。

新標(biāo)準(zhǔn)允許我們通過顯式實(shí)例化explicit instantiation來避免這種開銷嫩絮。

extern template declaration; // 實(shí)例化聲明
template declaration;        // 實(shí)例化定義

例子:

// 實(shí)例化聲明與定義
extern template class Blob<string>;            // 聲明
template int compare(const int&, const int&);  // 定義

當(dāng)編譯器遇到extern模板聲明時(shí)丛肢,他不會(huì)在本文件中生成實(shí)例化代碼,將一個(gè)實(shí)例化聲明為extern就表示承諾在程序其他位置有該實(shí)例化的一個(gè)非extern聲明(定義)剿干。對(duì)于一個(gè)給定的實(shí)例化版本蜂怎,可能有多個(gè)extern聲明,但必須只有一個(gè)定義置尔。對(duì)于一個(gè)給定的實(shí)例化版本杠步,可能有多個(gè)extern聲明,但必須只有一個(gè)定義榜轿。

由于編譯器在使用一個(gè)模板時(shí)自動(dòng)對(duì)齊實(shí)例化幽歼,因此extern聲明必須出現(xiàn)在任何使用此實(shí)例化版本的代碼之前:

// Application.cc
// 這些模板類型必須在程序其他位置進(jìn)行實(shí)例化
extern template class Blob<string>;
extern template int compare(const int&, const int&);
Blob<string> sa1, sa2;  // 實(shí)例化會(huì)出現(xiàn)在其他位置
// Blob<int>及其接受initializer_list的構(gòu)造函數(shù)在本文件中實(shí)例化
Blob<int> a1 = {0,1,2,3,4,5,6,7,8,9};
Blob<int> a2(a1);  // 拷貝構(gòu)造函數(shù)在本文件中實(shí)例化
int i = compare(a1[0], a2[0]);  // 實(shí)例化出現(xiàn)在其他位置

文件Application.o將包含Blob<int>的實(shí)例及其接受initializer_list參數(shù)的構(gòu)造喊你書和拷貝構(gòu)造函數(shù)的實(shí)例。而compare<int>函數(shù)和Blob<string>將不在本文件中進(jìn)行實(shí)例化谬盐,這些模板的定義必須出現(xiàn)在程序的其他文件中:

// templateBuild.cc
// 實(shí)例化文件必須為每個(gè)在其他文件中聲明為extern的類型和函數(shù)提供一個(gè)非extern的定義
template int compare(const int&, const int&);
template class Blob<string>; // 實(shí)例化類模板的所有成員

當(dāng)編譯器遇到一個(gè)實(shí)例化定義(與聲明相對(duì))時(shí)甸私,它為其生成代碼。因此飞傀,文件templateBuild.cc將會(huì)包含compareint實(shí)例化版本的定義和Blob<string>類的定義皇型。當(dāng)我們編譯此應(yīng)用程序時(shí),必須將templateBuild.oApplication.o鏈接到一起砸烦。

一個(gè)類模板的實(shí)例化定義會(huì)實(shí)例化該模板的所有成員弃鸦,包括內(nèi)聯(lián)的成員函數(shù)。與處理類模板的普通實(shí)例化不同幢痘,編譯器會(huì)實(shí)例化該類的所有成員唬格。即使我們不使用某個(gè)成員,它也會(huì)被實(shí)例化雪隧。因此在一個(gè)類模板的實(shí)例化定義中西轩,所用類型必須能用于模板的所有成員函數(shù)员舵。

模板實(shí)參推斷

1. 類型轉(zhuǎn)換與模板類型參數(shù)

能在調(diào)用中應(yīng)用于函數(shù)模板的包括如下三項(xiàng):

  • 頂層const無(wú)論是在形參中還是在實(shí)參中都會(huì)被忽略

  • const轉(zhuǎn)換:可以將一個(gè)非const對(duì)象的引用(或指針)傳遞給一個(gè)const的引用(或指針)形參

  • 數(shù)組或函數(shù)指針轉(zhuǎn)換:如果函數(shù)形參不是引用類型脑沿,則可以對(duì)數(shù)組或函數(shù)類型的實(shí)參應(yīng)用正常的指針轉(zhuǎn)換。一個(gè)數(shù)組形參可以轉(zhuǎn)換為一個(gè)指向其首元素的指針马僻,一個(gè)函數(shù)實(shí)參可以轉(zhuǎn)換為一個(gè)該函數(shù)類型的實(shí)參

將實(shí)參傳遞給待模板類型的函數(shù)形參時(shí)庄拇,能夠自動(dòng)應(yīng)用的類型轉(zhuǎn)換只有const轉(zhuǎn)換及數(shù)組或函數(shù)到指針的轉(zhuǎn)換。

1.1 使用相同模板參數(shù)類型的函數(shù)形參
// compare函數(shù)接受兩個(gè)const T&參數(shù), 其實(shí)參必須是相同類型
long lng;
compare(lng, 1024); // 錯(cuò)誤, 不能實(shí)例化compare(long, int)
1.2 正常類型轉(zhuǎn)換應(yīng)用于普通函數(shù)參數(shù)
template <typename T> ostream &print(ostream &os, const T &obj)
{
    return os << obj;
}

// 由于低于一個(gè)參數(shù)的類型不依賴于模板參數(shù), 因此編譯器會(huì)將f隱式轉(zhuǎn)換為ostream&
ofstream f("output");
print(f, 10);

2. 函數(shù)模板顯式實(shí)參

假設(shè)我們定義一個(gè)sum的函數(shù)模板,它接收兩個(gè)不同類型的參數(shù)措近,我們希望允許用戶指定結(jié)果的類型溶弟,這樣用戶就可以選擇合適的精度。我們可以定義表示返回類型的第三個(gè)模板參數(shù)瞭郑,從而允許控制返回類型:

// 編譯器無(wú)法推斷T1, 它不會(huì)出現(xiàn)在函數(shù)參數(shù)列表中
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3)

每次調(diào)用sum時(shí)調(diào)用者都必須為T1提供一個(gè)顯式模板實(shí)參:

auto val3 = sum<long long>(i, lng); // long long sum(int, long)

需要注意的是辜御,顯式模板實(shí)參按由左到右的順序與對(duì)應(yīng)的模板參數(shù)匹配:第一個(gè)模板實(shí)參與第一個(gè)模板參數(shù)匹配,第二個(gè)實(shí)參與第二個(gè)參數(shù)匹配屈张。只有尾部參數(shù)的顯示模板實(shí)參才可以忽略擒权,而且前提是它們可以從函數(shù)參數(shù)推斷出來。如果我們的sum函數(shù)按照如下形式編寫:

// 糟糕的設(shè)計(jì), 用戶必須指定所有三個(gè)模板參數(shù)
template <typename T1, typename T2, typename T3>
T3 alternative_sum(T1, T2);

// 錯(cuò)誤: 不能推斷前幾個(gè)模板參數(shù)
auto val3 = alternative_sum<long long>(i, lng);
// 正確: 顯式指定了所有三個(gè)參數(shù)
auto val2 = alternative_sum<long long, int, long>(i, lng);

正常類型轉(zhuǎn)換應(yīng)用于顯式指定的實(shí)參:

long lng;
compare(lng, 1024);       // 模板參數(shù)不匹配
compare<long>(lng, 1024); // 正確: 實(shí)例化compare(long, long);
compare<int>(lng, 1024);  // 正確: 實(shí)例化compare(int, int);

3. 尾置返回類型與類型轉(zhuǎn)換

3.1 尾置返回類型

當(dāng)我們希望用戶確定返回類型時(shí)阁谆,用顯式模板實(shí)參表示模板函數(shù)的返回類型是比較有效的碳抄,但是要求顯式指定模板實(shí)參會(huì)給用戶增添額外負(fù)擔(dān)。尾置返回允許我們?cè)趨?shù)列表之后聲明返回類型:

template <typename It>
auto fcn(It beg, It end) -> decltype(*beg) // 通知編譯器fcn的返回類型與解引用beg參數(shù)的結(jié)果類型相同, 解引用類型返回一個(gè)左值, 因此通過decltype推斷的類型為beg表示的元素的類型的引用
{
    // 處理負(fù)擔(dān)
    return *beg; // 返回序列中一個(gè)元素的引用
}
3.2 進(jìn)行類型轉(zhuǎn)換的標(biāo)準(zhǔn)庫(kù)模板類

在前面提到的例子中场绿,我們對(duì)傳遞的參數(shù)類型一無(wú)所知剖效,唯一可以使用的操作是迭代器操作,而所有的迭代器操作都不會(huì)生成元素焰盗,只能生成元素的引用璧尸。

為了獲得元素類型,我們可以使用標(biāo)準(zhǔn)庫(kù)的類型轉(zhuǎn)換type transformation模板姨谷,這些模板定義在type_traits中逗宁。

在本例中,我們可以使用remove_reference來獲得元素類型梦湘。remove_reference模板有一個(gè)模板類型參數(shù)和一個(gè)名為typepublic類型成員瞎颗。如果我們用一個(gè)引用類型實(shí)例化它,那么type表示被引用的類型捌议。我們重寫一個(gè)函數(shù)以返回元素值的拷貝而不是引用:

template <typename It>
auto fcn2(It beg, It end) ->
    typename remove_reference<decltype(*beg)>::type
{
    // 處理序列
    return *beg;  // 返回序列中一個(gè)元素的拷貝
}
對(duì)Mod<T>哼拔,其中Mod T Mod<T>::type
remove_reference X&X&&<br />否則 X<br />T
add_const X&const X或函數(shù)<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 帶符號(hào)類型<br />否則 unsigned X<br />T
remove_extent X[n]<br />否則 X<br />T
remove_all_extents X[n1][n2]...<br />否則 X<br />T

4.函數(shù)指針和實(shí)參推斷

當(dāng)我們用一個(gè)函數(shù)模板初始化一個(gè)函數(shù)指針或者為一個(gè)函數(shù)指針賦值時(shí)瓣颅,編譯器使用指針的類型來推斷模板實(shí)參倦逐。

template <typename T> int compare(const T&, const T&);
// pf1指向?qū)嵗齣nt compare(const int&, const int&)
int (*pf1)(const int&, const int&) = compare;

如果不能從函數(shù)指針類型確定模板實(shí)參,那么會(huì)產(chǎn)生錯(cuò)誤:

// func的重載版本: 每個(gè)版本接受一個(gè)不同的函數(shù)指針類型
void func(int(*)(const string&, const string&));
void func(int(*)(const int&, const int&));
func(compare); // 錯(cuò)誤: 使用compare哪個(gè)實(shí)例?

// 我們可以通過顯式模板實(shí)參來消除func調(diào)用的歧義
func(compare<int>); // 傳遞compare(const int&, const int&)

5.模板實(shí)參推斷和引用

為了理解如何從函數(shù)調(diào)用進(jìn)行類型推斷宫补,考慮下面例子:

template <typename T> void f(T &p);

其中函數(shù)參數(shù)p是一個(gè)模板類型參數(shù)T的引用檬姥,需要記住非常重要的兩點(diǎn):

  • 編譯器會(huì)應(yīng)用正常的引用綁定規(guī)則
  • const是底層的,不是頂層的
5.1 從左值引用函數(shù)參數(shù)推斷類型

當(dāng)一個(gè)函數(shù)參數(shù)是模板類型參數(shù)的一個(gè)普通(左值)引用時(shí)(即形如T&)粉怕,綁定規(guī)則告訴我們只能給它一個(gè)左值(比如一個(gè)變量或者一個(gè)返回引用類型的表達(dá)式)健民。實(shí)參可以是const類型也可以不是。如果實(shí)參是const的贫贝,那么T將會(huì)被推斷為const

template <typename T> void f1(T&);   // 實(shí)參必須是一個(gè)左值
f1(i);   // i是一個(gè)int; 模板參數(shù)類型T是int
f1(ci);  // ci是一個(gè)const int; 模板參數(shù)T是const int
f1(5);   // 錯(cuò)誤: 傳遞給一個(gè)&參數(shù)的實(shí)參必須是一個(gè)左值

如果一個(gè)函數(shù)參數(shù)的類型是const T&秉犹,正常的綁定規(guī)則告訴我們可以傳遞給它任何類型的實(shí)參——一個(gè)對(duì)象(const或者非const)蛉谜、一個(gè)臨時(shí)對(duì)象或是一個(gè)字面常量值。當(dāng)函數(shù)參數(shù)本身是const時(shí)崇堵,T的類型推斷的結(jié)果不會(huì)是一個(gè)const型诚,const已經(jīng)是函數(shù)參數(shù)類型的一部分,因此它不會(huì)也是模板參數(shù)類型的一部分:

template <typename T> void f2(const T&);   // 可以接受一個(gè)右值
// f2中的參數(shù)是const &; 實(shí)參中的const是無(wú)關(guān)的
// 在每個(gè)調(diào)用中, f2的函數(shù)參數(shù)都被推斷為const int&
f2(i);   // i是一個(gè)int;模板參數(shù)T是int
f2(ci);  // ci是一個(gè)const int, 但模板參數(shù)T是int
f2(5);   // 一個(gè)const &參數(shù)可以綁定到一個(gè)右值; T是int
5.2 從右值引用函數(shù)參數(shù)推斷類型

當(dāng)一個(gè)函數(shù)參數(shù)是一個(gè)右值引用(即形如T&&)鸳劳,正常綁定規(guī)則告訴我們可以傳遞給它一個(gè)右值:

template <typename T> void f3(T&&);
f3(42);   // 實(shí)參是一個(gè)int類型的右值; 模板參數(shù)T是int
5.3 引用折疊和右值引用參數(shù)

假定i是一個(gè)int對(duì)象狰贯,我們可能認(rèn)為像f3(i)這樣的調(diào)用是不合法的。畢竟i是一個(gè)左值赏廓,而通常我們不能將一個(gè)右值引用綁定到一個(gè)左值上暮现。但是C++在正常綁定規(guī)則外定義了兩個(gè)例外規(guī)則,允許這種綁定:

  • 第一個(gè)例外規(guī)則:當(dāng)我們將一個(gè)左值(如i)傳遞給函數(shù)的右值引用參數(shù)楚昭,且此右值引用指向模板類型參數(shù)(如T&&)時(shí)栖袋,編譯器推斷模板類型參數(shù)為實(shí)參的左值引用類型。因此抚太,當(dāng)我們調(diào)用f3(i))時(shí)塘幅,編譯器推斷T的類型為int&而非int

通常我們不能(直接)定義一個(gè)引用的引用,但是通過類型別名或通過模板類型參數(shù)間接定義是可以的

  • 第二個(gè)例外規(guī)則:如果我們間接創(chuàng)建一個(gè)引用的引用尿贫,則這些引用形成了“折疊”电媳,在所有情況下(除了一個(gè)例外),引用會(huì)折疊成一個(gè)普通的左值引用類型庆亡。只有一種情況下回折疊成右值引用:右值引用的右值引用匾乓。

對(duì)于一個(gè)給定類型X

  • X& &X& &&X&& &都折疊成X&
  • X&& &&折疊成X&&

這兩個(gè)規(guī)則導(dǎo)致了兩個(gè)重要結(jié)果:

  • 如果一個(gè)函數(shù)參數(shù)是一個(gè)指向模板類型參數(shù)的右值引用(即如T&&)又谋,則它可以被綁定到一個(gè)左值
  • 如果實(shí)參是一個(gè)左值拼缝,則推斷出模板實(shí)參類型將是一個(gè)左值引用,且函數(shù)參數(shù)將被實(shí)例化為一個(gè)(普通)左值引用參數(shù)(T&

這兩個(gè)規(guī)則暗示我們將任意類型的實(shí)參傳遞給T&&類型的函數(shù)參數(shù)彰亥,對(duì)于這種類型的參數(shù)咧七,(顯然)可以傳遞給它右值,也可以傳遞給它左值任斋。

5.4 編寫接收右值引用參數(shù)的模板函數(shù)

如果一個(gè)函數(shù)參數(shù)是一個(gè)指向模板類型參數(shù)的右值引用(即如T&&)继阻,模板內(nèi)的代碼就會(huì)產(chǎn)生歧義:

template <typename T> void f3(T&& val)
{
    T t = val; // 實(shí)參是左值時(shí), 模板參數(shù)T是int&, 那么是綁定一個(gè)引用; 實(shí)參是右值時(shí), 模板參數(shù)T是int, 那么是拷貝val值到t
    t = fcn(t);   // 賦值是只改變t還是既改變t又改變val
    if (val == t) { /*...*/ }  // 如果T是引用類型, 則一直為true
}

如上所述,當(dāng)代碼中涉及的類型可能是普通(非引用)類型废酷,也可能是引用類型時(shí)瘟檩,編寫正確的代碼就變得異常困難(雖然remove_reference這樣的類型轉(zhuǎn)換可能有所有幫助)。

在實(shí)際中澈蟆,右值引用通常用于兩種情況:模板轉(zhuǎn)發(fā)其實(shí)參或模板被重載墨辛。使用右值引用的函數(shù)模板通常要進(jìn)行重載:

// 右值會(huì)調(diào)用第一個(gè)函數(shù), 排除歧義的問題
template <typename T> void f(T&&);       // 綁定到非const右值
template <typename T> void f(const T&);  // 左值和cosnt右值

6. 理解std::move

雖然不能直接將一個(gè)右值引用綁定到一個(gè)左值上,但可以用move獲得一個(gè)綁定到左值上的右值引用丰介。

6.1 std::move在標(biāo)準(zhǔn)庫(kù)中的定義
// 在返回類型和類型轉(zhuǎn)換中也要用到typename
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
    return static_cast<typename remove_reference<T>::type&&>(t);
}

move的函數(shù)參數(shù)T&&是一個(gè)指向模板類型參數(shù)的右值引用背蟆,通過引用折疊意味著該參數(shù)可以與任何類型的實(shí)參匹配。特別是我們既可以給move傳遞一個(gè)左值哮幢,又可以給它傳遞一個(gè)右值:

string s1("hi!"), s2;
s2 = std::move(string("bye!"))带膀;   // 正確: 從一個(gè)右值移動(dòng)數(shù)據(jù)
s2 = std::move(s1);                // 正確: 但是賦值之后, s1的值是不確定的
6.2 std::move是如何工作的

仍然看上面的例子:

s2 = std::move(string("bye!"));
  • 推斷出T的類型是string
  • remove_referencestring實(shí)例化
  • remove_referencetype成員是string
  • move返回類型是string &&
  • move的函數(shù)參數(shù)t的類型為string&&

因此這個(gè)調(diào)用實(shí)例化即函數(shù):

string&& move(string &&t)
// 參數(shù)t的類型已經(jīng)是string&&, 因此函數(shù)體類型轉(zhuǎn)換什么都不做, 返回它所接受的右值引用

看第二個(gè)例子:

s2 = std::move(s1); 
  • 推斷出T的類型是string &
  • remove_referencestring&實(shí)例化
  • remove_reference<string&>type成員是string
  • move返回類型string &&
  • move的函數(shù)參數(shù)t實(shí)例化為string& &&橙垢,會(huì)折疊成string &

因此這個(gè)調(diào)用實(shí)例化即:

string&& move(string &t)
6.3 從一個(gè)左值static_cast到一個(gè)右值引用是允許的
  • 雖然不能隱式地將一個(gè)左值轉(zhuǎn)換成右值引用垛叨,但是我們可以用static_cast顯式轉(zhuǎn)換
  • 使用static_cast顯式將一個(gè)左值轉(zhuǎn)換成右值引用,會(huì)截?cái)嘁粋€(gè)左值柜某,只有當(dāng)你確保截?cái)嗪笫前踩牟胚@么操作
  • 使用std::move使我們?cè)诔绦?中查找潛在的截?cái)嘧笾档拇a很容易

7. 轉(zhuǎn)發(fā)

某些函數(shù)需要將其一個(gè)或多個(gè)實(shí)參聯(lián)通類型不變地轉(zhuǎn)發(fā)給其他參數(shù)嗽元,這種情況我們需要保持被轉(zhuǎn)發(fā)實(shí)參的所有性質(zhì):

  • 實(shí)參類型是不是const
  • 實(shí)參是左值還是右值

看一下這個(gè)例子,我們編寫一個(gè)函數(shù)接受一個(gè)可調(diào)用表達(dá)式和兩個(gè)額外實(shí)參:

// 對(duì)"翻轉(zhuǎn)"的參數(shù)調(diào)用給定的可調(diào)用對(duì)相關(guān)
// flip1是一個(gè)不完整的實(shí)現(xiàn): 頂層const和引用丟失了
template <typename F, typename T1, typename T2>
void flip1(F f, T1 t1, T2 t2)
{
    f(t2, t1);
}

這個(gè)函數(shù)一般工作得很好喂击,但當(dāng)我們希望用它調(diào)用一個(gè)接受引用參數(shù)的函數(shù)就會(huì)出現(xiàn)問題:

void f(int v1, int &v2) // v2是一個(gè)引用
{
    cout << v1 << " " << ++v2 << endl;
}

當(dāng)我們通過flip1調(diào)用f時(shí)就會(huì)丟失v2的引用信息剂癌。

我們可以使用forward的新標(biāo)準(zhǔn)庫(kù)來傳遞flip2的參數(shù),它能保持原始參數(shù)的類型翰绊。與move不同的是佩谷,forward必須通過顯式模板實(shí)參來調(diào)用,forward返回該顯式實(shí)參類型的右值引用监嗜。即forward<T>的返回類型是T&&谐檀。通過返回類型上的引用折疊,forward可以保持給定實(shí)參的左值/右值屬性裁奇。

template <typename Type> intermediary(Type &&arg)
{
    finalFcn(std::forward<Type>(arg));
    // ...
}

我們使用Type作為forward的顯式模板實(shí)參類型桐猬,它是從arg推斷出來的。由于arg是一個(gè)模板類型參數(shù)的右值引用刽肠,Type將表示傳遞給arg的實(shí)參的所有類型信息:

  • 如果實(shí)參是一個(gè)右值溃肪,那么Type是一個(gè)普通(非引用)類型,forward<Type>將返回Type&&
  • 如果實(shí)參是一個(gè)左值音五,那么通過引用折疊乍惊,Type本身是一個(gè)左值引用類型,forward返回類型是一個(gè)指向左值引用類型的右值引用放仗,折疊后返回一個(gè)左值引用類型

當(dāng)用于一個(gè)指向模板參數(shù)類型的右值引用函數(shù)參數(shù)T&&時(shí)润绎,forward會(huì)保持實(shí)參類型的所有細(xì)節(jié)。使用forward诞挨,我們可以再次重寫反轉(zhuǎn)函數(shù):

template <typename F, typename T1, typename T2>
void flip(F f, T1 &&t1, T2 &&t2)
{
    f(std::forward<T2>(t2), std::forward<T1>(t1));
}

重載與模板

函數(shù)模板可以被另一個(gè)模板或一個(gè)普通非模板函數(shù)重載莉撇。與之前一樣,名字相同的函數(shù)必須具有不同數(shù)量或類型的參數(shù)惶傻。

1. 編寫重載模板

我們構(gòu)造一組調(diào)試函數(shù)命名為debug_rep棍郎,每個(gè)函數(shù)返回一個(gè)給定對(duì)象的string 表示。我們首先編寫此函數(shù)的最通用版本银室,并將它定義為接受一個(gè)const對(duì)象引用的模板:

template <typename T> string debug_rep(const T &t)
{
    ostringstream ret;
    ret << t;  // 使用T的輸出運(yùn)算符打印t的一個(gè)表示形式
    return ret.str();  // 返回ret綁定的string的一個(gè)副本
}

再定義打印指針的版本:

// 注意此函數(shù)不能用于char*對(duì)象, 因?yàn)镮O庫(kù)為char*定義了一個(gè)<<版本, 此版本假定指針表示一個(gè)空字符結(jié)尾的字符數(shù)組, 并打印數(shù)組的內(nèi)容而非地址
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();
}

使用:

// 只有第一個(gè)版本是可行的, 因?yàn)榫幾g器無(wú)法從一個(gè)費(fèi)指針參數(shù)實(shí)例化一個(gè)期望值真類型參數(shù)的函數(shù)模板
string s("hi");
cout << debug_rep(s) << endl;

// 如果用指針調(diào)用, 則兩個(gè)版本都是可行的
cout << debug_rep(&s) << endl;
// 第一個(gè)版本的T被綁定到string*, 實(shí)例化debug_rep(const string*&)
// 第二個(gè)版本的T被綁定到string, 實(shí)例化debug_rep(string*)

第一個(gè)版本的實(shí)例需要進(jìn)行普通指針到const指針的轉(zhuǎn)換涂佃,正常函數(shù)匹配規(guī)則告訴我們應(yīng)該選擇第二個(gè)版本励翼。

2. 多個(gè)可行模板

當(dāng)多個(gè)重載模板對(duì)一個(gè)調(diào)用提供同樣好的匹配時(shí),應(yīng)選擇最特例化的版本辜荠。

3. 非模板和模板重載

對(duì)于一個(gè)調(diào)用汽抚,如果一個(gè)非函數(shù)模板與一個(gè)函數(shù)模板提供同樣好的匹配,則選擇非模板版本伯病。

4. 重載模板與類型轉(zhuǎn)換

考慮一下C風(fēng)格字符串指針和字符串字面常量造烁。考慮一下這個(gè)調(diào)用:

cout << debug_rep("hi world!") << endl;  // 調(diào)用debug_rep(T*)

有三個(gè)debug_rep版本都是可行的:

  • debug_rep(const T&)午笛,T被綁定到char[10]
  • debug_rep(T*)惭蟋,T被綁定到const char
  • debug_rep(const sring&),要求從const char*string的類型轉(zhuǎn)換

對(duì)給定實(shí)參來說药磺,兩個(gè)模板都提供精確匹配(第二個(gè)模板需要進(jìn)行一次數(shù)組到指針的轉(zhuǎn)換告组,對(duì)于函數(shù)匹配來說,這種轉(zhuǎn)換被認(rèn)為是精確匹配)癌佩。非模板版本是可行的惹谐,但是需要進(jìn)行一次用戶定義的類型轉(zhuǎn)換,因此沒有精確匹配那么好驼卖,所以兩個(gè)模板稱為可能被調(diào)用的函數(shù)氨肌。由于第二個(gè)模板T*更加特例化,因此編譯器會(huì)選擇它酌畜。

5. 缺少聲明可能導(dǎo)致程序行為異常

通常如果使用了一個(gè)忘記聲明的函數(shù)怎囚,代碼將編譯失敗。但是對(duì)于重載函數(shù)模板的函數(shù)而言桥胞,如果編譯器可以從模板實(shí)例化出與調(diào)用匹配的版本恳守,則缺少的聲明就不會(huì)報(bào)錯(cuò)。以前面的例子而言贩虾,如果缺少了接收T*的模板版本催烘,則編譯器會(huì)默認(rèn)實(shí)例化接受const T&的模板版本。

在定義任何函數(shù)之前缎罢,記得聲明所有重載的函數(shù)版本伊群。這樣就不必?fù)?dān)心編譯器由于未遇到你希望調(diào)用的函數(shù)而實(shí)例化一個(gè)并非你需要的版本。

可變參數(shù)模板

一個(gè)可變參數(shù)模板variadic template就是一個(gè)接受可變數(shù)組參數(shù)的模板函數(shù)或模板類策精〗⑹迹可變數(shù)目的參數(shù)被稱為參數(shù)包parameter packet,參數(shù)包包括模板參數(shù)包和函數(shù)參數(shù)包咽袜。

// Args是一個(gè)模板參數(shù)包; rest是一個(gè)函數(shù)參數(shù)包
// Args表示零個(gè)或多個(gè)模板類型參數(shù)
// rest表示零個(gè)或多個(gè)函數(shù)參數(shù)
template <typename T, typename... Args>
void foo(const T &t, const Args& ... rest)

// 調(diào)用方式
int i = 0; double d = 3.14; string s = "how now brown cow";
foo(i, s, 42, d);   // 包中三個(gè)參數(shù)
foo(s, 42, "hi");   // 包中兩個(gè)參數(shù)
foo(d, s);          // 包中一個(gè)參數(shù)
foo("hi");          // 空包

// 編譯器會(huì)分別實(shí)例化對(duì)應(yīng)的版本
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...運(yùn)算符

當(dāng)我們需要知道包中有多少元素時(shí)丸卷,可以使用sizeof...運(yùn)算符:

template <typename ... Args> void g(Args ... args) {
    cout << sizeof...(Args) << endl;  // 類型參數(shù)的數(shù)目
    cout << sizeof...(args) << endl;  // 函數(shù)參數(shù)的數(shù)目
}

2. 編寫可變參數(shù)函數(shù)模板

// 用于終止遞歸并打印最后一個(gè)元素的函數(shù)
// 此函數(shù)必須在可變參數(shù)版本的print定義之前聲明
template <typename T>
ostream &print(ostream &os, const T &t)
{
    return os << t; // 包中最后一個(gè)元素之后不打印分隔符
}

// 包中除了最后一個(gè)元素之外的其他元素都會(huì)調(diào)用這個(gè)版本的print
template <typename T, typename... Args>
ostream &print(ostream &os, const T &t, const Args&... rest)
{
    os << t << ", ";    // 打印第一個(gè)實(shí)參
    return print(os, rset...); // 遞歸調(diào)用, 打印其他實(shí)參
}
// 調(diào)用:
print(cout, i, s, 42);  // 包中有兩個(gè)參數(shù)
// 依次執(zhí)行:
print(cout, i, s, 42);  // t:i, rset:s, 42
print(cout, s, 42);     // t:s, rset:42
print(cout, 42);        // 調(diào)用非可變參數(shù)版本的print
  • 對(duì)于最后一個(gè)調(diào)用,兩個(gè)函數(shù)提供同樣好的匹配询刹,但是非可變參數(shù)模板比可變參數(shù)模板更加特例化谜嫉,因此編譯器選擇非可變參數(shù)版本
  • 當(dāng)定義可變參數(shù)版本的print時(shí)萎坷,非可變參數(shù)版本的聲明必須在作用域中,否則可變參數(shù)版本會(huì)無(wú)限遞歸

3. 包擴(kuò)展

我們前面提到的print函數(shù)包含兩個(gè)擴(kuò)展:

template <typename T, typename... Args>
ostream &print(ostream &os, const T &t, const Args&... rest) // 擴(kuò)展Args
{
    os << t << ", ";   
    return print(os, rset...);                               // 擴(kuò)展rest
}

C++還支持更復(fù)雜的擴(kuò)展模式沐兰,我們可以編寫第二個(gè)可變參數(shù)函數(shù)哆档,對(duì)其每個(gè)實(shí)參調(diào)用debug_rep,然后調(diào)用print打印結(jié)果的string

// 在print調(diào)用中對(duì)每個(gè)實(shí)參調(diào)用debug_rep
template <typename... Args>
ostream &errorMsg(ostream &os, const Args&... rest)
{
    // 等價(jià)于print(os, debug_rep(a1), debug_rep(a2), ..., debug_rep(an))
    return print(os, debug_rep(rset)...);
}

4. 轉(zhuǎn)發(fā)參數(shù)包

可變參數(shù)函數(shù)通常將它們的參數(shù)轉(zhuǎn)發(fā)給其他函數(shù)僧鲁,這種函數(shù)通常與我們的emplace_back函數(shù)具有一樣的形式:

// fun有零個(gè)或多個(gè)參數(shù), 每個(gè)參數(shù)都是一個(gè)模板參數(shù)類型的右值引用
template<typename... Args>
void fun(Args&&... args)   // 將Args擴(kuò)展為一個(gè)右值引用的列表
{
    // work的實(shí)參既擴(kuò)展Args又?jǐn)U展args
    work(std::forward<Args>(args)...);
}

我們希望將fun的所有實(shí)參轉(zhuǎn)發(fā)給另一個(gè)名為work的函數(shù),假定它完成函數(shù)的實(shí)際工作象泵。類似于emplace_back中對(duì)construct的調(diào)用寞秃,work調(diào)用中的擴(kuò)展既擴(kuò)展了模板參數(shù)包也擴(kuò)展了函數(shù)參數(shù)包。由于fun的參數(shù)是右值引用偶惠,因此我們可以傳遞給它任意類型的實(shí)參春寿,由于我們使用std::forward傳遞這些實(shí)參,因此它們的所有信息類型在調(diào)用work時(shí)都會(huì)得到保持忽孽。

模板特例化

繼續(xù)看我們之前定義的compare函數(shù):

// 第一個(gè)版本: 可以比較任意兩個(gè)類型
template <typename T> int compare(const T&, const T&);
// 第二個(gè)版本處理字符串字面常量
template<size_t N, size_t M>
int compare(const char (&)[N], const char (&)[M]);

只有當(dāng)我們傳遞給compare一個(gè)字符串字面常量或者一個(gè)數(shù)組時(shí)绑改,編譯器才會(huì)調(diào)用第二個(gè)版本,如果我們傳遞給它字符指針兄一,就會(huì)調(diào)用第一個(gè)版本(我們無(wú)法將一個(gè)指針轉(zhuǎn)換為一個(gè)數(shù)組的引用):

const char *p1 = "hi", *p2 = "mom";
compare(p1, p2);      // 調(diào)用第一個(gè)模板版本
compare("hi", "mom"); // 調(diào)用第二個(gè)版本

1. 定義函數(shù)模板特例化

為了處理字符指針(而不是數(shù)組)厘线,可以為第一個(gè)版本的compare定義一個(gè)模板特例化版本。

模板特例化版本就是模板的一個(gè)獨(dú)立的定義出革,在其中一個(gè)或多個(gè)模板參數(shù)被指定為特定的類型造壮。

// compare的特殊版本, 處理字符數(shù)組的指針
template<>
int compare(const char* const&p1, const char* const &p2)
{
    return strcmp(p1, p2);
}

一個(gè)特例化的版本本質(zhì)上是一個(gè)實(shí)例庶艾,而非函數(shù)名的一個(gè)重載版本秘蛇。因此特例化不影響函數(shù)匹配。另外需要注意的是:

  • 為了特例化一個(gè)模板格郁,原模板的聲明必須在作用域之中展箱;在任何使用模板實(shí)例的代碼之前旨枯,特例化版本的聲明也必須在作用域中
  • 從前一條得知:模板及其特例化版本應(yīng)該聲明在同一個(gè)頭文件中,所有同名模板的聲明應(yīng)該放在前面混驰,然后是這些模板的特例化版本

2. 類模板特例化

除了特例化函數(shù)模板攀隔,我們還可以特例化類模板。唯一需要注意的是栖榨,我們必須在原模板定義所在的命名空間中特例化它竞慢。

3. 類模板部分特例化

與函數(shù)模板不同,類模板的特例化不必為所有模板參數(shù)提供實(shí)參治泥。我們可以只指定一部分而非所有模板參數(shù)筹煮,或是參數(shù)的一部分而非全部特性。一個(gè)類模板的部分特例化本身是一個(gè)模板居夹,使用它時(shí)用戶還必須為那些在特例化版本中未指出的模板參數(shù)提供實(shí)參:

// 原始的通用的版本
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; };
    
// 調(diào)用
int i;
// decltype(42)為int, 使用通用版本
remove_reference<decltype(42)>::type a;
// decltype(i)為int&, 使用第一個(gè)版本T&
remove_reference<decltype(i)>::type b;
// decltype(std::move(i))為int&&, 使用第二個(gè)版本T&&
remove_reference<decltype(std::move(i))>::type c;

4. 特例化成員而不是類

假定Foo是一個(gè)模板類败潦,包含一個(gè)成員Bar本冲,我們可以只特例化該成員:

template <typename T> struct Foo {
    Foo(const T &t = T()) : mem(t) { }
    void Bar() { /*...*/ }
    T mem;
    // Foo其他成員
};
template<>       // 我們正在特例化一個(gè)模板
void Foo<int>::Bar()   // 我們正在特例化Foo<int>的成員Bar
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市劫扒,隨后出現(xiàn)的幾起案子檬洞,更是在濱河造成了極大的恐慌,老刑警劉巖沟饥,帶你破解...
    沈念sama閱讀 212,542評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件添怔,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡贤旷,警方通過查閱死者的電腦和手機(jī)广料,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來幼驶,“玉大人艾杏,你說我怎么就攤上這事≈言澹” “怎么了购桑?”我有些...
    開封第一講書人閱讀 158,021評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)氏淑。 經(jīng)常有香客問我勃蜘,道長(zhǎng),這世上最難降的妖魔是什么假残? 我笑而不...
    開封第一講書人閱讀 56,682評(píng)論 1 284
  • 正文 為了忘掉前任元旬,我火速辦了婚禮,結(jié)果婚禮上守问,老公的妹妹穿的比我還像新娘匀归。我一直安慰自己,他們只是感情好耗帕,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,792評(píng)論 6 386
  • 文/花漫 我一把揭開白布穆端。 她就那樣靜靜地躺著,像睡著了一般仿便。 火紅的嫁衣襯著肌膚如雪体啰。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,985評(píng)論 1 291
  • 那天嗽仪,我揣著相機(jī)與錄音荒勇,去河邊找鬼。 笑死闻坚,一個(gè)胖子當(dāng)著我的面吹牛沽翔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 39,107評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼仅偎,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼跨蟹!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起橘沥,我...
    開封第一講書人閱讀 37,845評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤窗轩,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后座咆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體痢艺,經(jīng)...
    沈念sama閱讀 44,299評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,612評(píng)論 2 327
  • 正文 我和宋清朗相戀三年介陶,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了堤舒。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,747評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡斤蔓,死狀恐怖植酥,靈堂內(nèi)的尸體忽然破棺而出镀岛,到底是詐尸還是另有隱情弦牡,我是刑警寧澤,帶...
    沈念sama閱讀 34,441評(píng)論 4 333
  • 正文 年R本政府宣布漂羊,位于F島的核電站驾锰,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏走越。R本人自食惡果不足惜椭豫,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,072評(píng)論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望旨指。 院中可真熱鬧赏酥,春花似錦、人聲如沸谆构。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)搬素。三九已至呵晨,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間熬尺,已是汗流浹背摸屠。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評(píng)論 1 267
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留粱哼,地道東北人季二。 一個(gè)月前我還...
    沈念sama閱讀 46,545評(píng)論 2 362
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像揭措,于是被迫代替她去往敵國(guó)和親戒傻。 傳聞我的和親對(duì)象是個(gè)殘疾皇子税手,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,658評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容