C++模板與泛型編程:可變參數模板

可變參數模板

​ 一個可變參數模板就是接受一個可變數目參數的模板函數或模板類。可變數目的參數被稱爲參數包。存在兩種參數包:模板參數包,表示零個或多個模板參數;函數參數包,表示零個或多個函數參數。

​ 我們用一個省略號來指出一個模板參數或函數參數表示一個包。在一個模板參數列表中,class… 或 typename… 指出接下來的參數表示零個或多個類型的列表;一個類型名後面跟一個省略號表示零個或多個給定類型的非類型參數的列表。在函數參數列表中,如果一個參數的類型是一個模板參數包,則此參數也是一個函數參數包。例如:

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

聲明瞭 foo 是一個可變參數函數模板,它有一個名爲 T 的類型參數,和一個名爲 Args 的模板參數包。這個包表示零個或多個額外的類型參數。foo 的函數參數列表包含一個 const & 類型的參數,指向 T 的類型,還包含一個名爲 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");			// 空包

編譯器會爲 foo 實例化出四個不同的版本:

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]&);

在每個實例中,T 的類型都是從第一個實參的類型推斷出來的。剩下的實參提供函數額外實參的數目和類型。

sizeof… 運算符

​ 當我們需要知道包中有多少元素時,可以用 sizeof… 運算符。sizeof… 返回一個常量表達式,而且不會對其實參求值:

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

編寫可變參數函數模板

​ 我們知道,可以用 initializer_list 來定義一個可接受可變數目實參的函數。但是,所有實參必須具有相同的類型(或它們的類型可以轉換爲一個公共類型)。

​ 所以,當我們既不知道想要處理的實參的數目也不知道它們的類型時,可變參數函數是很有用的。作爲一個例子,我們將定義一個名爲 print 的函數,它在一個給定流上打印給定實參列表的內容。

​ 可變參數函數通常是遞歸的。第一步調用處理包中的第一個實參,然後用剩餘實參調用自身。我們的 print 函數也是如此。爲了終止遞歸,我們還需要定義一個非可變參數的 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, rest...);		// 遞歸調用,打印其他實參
}

​ 這段程序的關鍵部分是可變參數函數中對 print 的調用:

return print(os, rest...);

我們可以發現,可變參數版本的 print 有三個參數,os,const T& 與 一個參數包。而此調用只傳遞了兩個實參。其結果是 rest 中的第一個實參被綁定到 t,剩餘實參形成下一個 print 調用的參數包。當此包中只剩下一個參數時,雖然兩個版本的 print 都能夠精確匹配,但是非函數模板優先於函數模板,所以最後一個 print 調用的非函數模板的 print。

當定義可變參數版本的 print 時,非可變參數版本的聲明必須在作用域中。否則,可變參數版本可能會無限遞歸。

包擴展

​ 對於一個參數包,除了獲取其大小外,我們對它做的唯一的事情就是擴展它。當擴展一個包時,我們還要提供用於每個擴展元素的模式擴展一個包就是將它分解爲構成的元素,對每個元素應用模式,獲得擴展後的列表。我們通過在模式右邊放一個省略號(…) 來觸發擴展操作

​ 例如,我們的 print 函數包含兩個擴展:

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

​ print 中的函數參數包擴展僅僅將包擴展爲其構成元素,C++還允許更爲複雜的擴展模式。例如,我們可以編寫第二個可變參數函數,對其每個實參調用 debug_rep,然後調用 print 打印結果 string:

template <typename T> string debug_rep(const T&t) {
    ostringstream ret;
    ret << t;
    return ret.str();
}
template <typename T> string debug_rep(T *p) {
    ostringstream ret;
    ret << "pointer: " << p;
    if(p) ret << " " << debug_rep(*p);	// 打印 p 指向的值
    else ret << " null pointer";
    return ret.str();
}
string debug_rep(const string &s) { return '"' + s + '"'; }
string debug_rep(char *p) { return debug_rep(string(p)); }
string debug_rep(const char *p) { return debug_rep(string(p)); }
// 以上是 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(rest)...);
}

​ 這個 print 調用了使用模式 debug_reg(rest)。此模式表示我們希望對函數參數包 rest 中的每個元素調用 debug_rep。擴展結果將是一個逗號分隔的 debug_rep 調用列表。即,下面調用:

errorMsg(cerr,fcnName,code.num(),otherDate,"other",item);

就好像我們這樣編寫代碼一樣:

print(cerr,debug_rep(fcnName),debug_rep(code.num()),debug_rep(otherData),
     debug_rep("other"),debug_rep(item));

與之相對,下面的模式會編譯失敗:

// 將包傳遞給 debug_rep; print(os,debug_rep(a1,a2,...,an))
print(os,debug_rep(rest...));		// 錯誤,此調用無匹配函數

這段代碼的問題是我們在 debug_rep 調用中擴展了 rest,它等價於:

print(cerr, debug_rep(fcnName,code.num(),otherData,"other",item));

在這個擴展中,我們試圖用一個五個實參的列表來調用 debug_rep,但並不存在與此調用匹配的 debug_rep 版本。

轉發參數包 (emplace_back實現)

在新標準下,我們可以組合使用可變參數模板與 forward 機制來編寫函數,實現將其實參不變地傳遞給其他函數。作爲例子,我們將爲 StrVec 類添加一個 emplace_back 成員。標準庫容器的 emplace_back 成員是一個可變參數成員模板,它用其實參管理的內存空間中直接構造一個元素。

// 這裏的 StrVec類省略部分成員,只提供 emplace_back 所需求的成員
class StrVec {
public:
    // 省略
private:
    static std::allocator<std::string> alloc;
    std::pair<std::string*, std::string*> alloc_n_copy
            (const std::string*, const std::string*);
    void chk_n_alloc();			// 保證內存能夠存儲元素
    std::string *elements;      // 指向分配的內存中的首元素
    std::string *first_free;    // 指向最後一個實際元素之後的位置
    std::string *cap;           // 指向分配的內存末尾之後的位置
};

​ 我們爲 StrVec 設計的 emplace_back 版本也應該是可變參數的,因爲 string 有多個構造函數,參數各不相同。由於我們希望能使用 string 的移動構造函數,因此還需要保存傳遞給 emplace_back 的實參的所有類型信息。

​ 如我們所知道的,保持類型信息是一個兩階段的過程。首先,爲了保持實參中的類型信息,必須將 emplace_back 的函數參數定義爲模板類型參數的右值引用:

class StrVec {
public:
    template <class... Args> void emplace_back(Args&&...);
    // 其他成員這裏省略
};

模板參數包擴展中的模式是 &&,意味着每個函數參數將是一個指向其對應實參的右值引用。

​ 其次,當 emplace_back 將這些實參傳遞給 construct 時,我們必須使用 forward 來保持實參的原始類型:

template <class... Args>
inline void StrVec::emplace_back(Args&& ...args) {
    chk_n_alloc();		// 如果需要的話重新分配 StrVec 的內存空間
    alloc.construct(first_free++,std::forward<Args>(args)...);
    // construct本身也是構造,與 emplace_back 類似
}

我們可以發現,construct 中的擴展爲:std::forward<Args>(args)…

它既擴展了模板參數包 Args,也擴展了函數參數包 args。此模式生成如下形式的元素:

std::forward<Ti>(ti);

其中 Ti 表示模板參數包中第 i 個元素的類型,ti 表示函數參數包中第 i 個元素。例如,假定 svec 是一個 StrVec,如果我們調用:svec.emplace_back(10,‘c’);

construct 調用中的模式會擴展出:

std::forward<int>(10),std::forward<char>(c)

​ 通過在此調用中使用 forward,我們保證如果用一個右值引用調用 emplace_back,則 construct 也會得到一個右值 (這是 forward 可以保證的)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章