一、可變參數模板概述
- 一個可變參數模板就是:一個接受可變數目參數的模板函數或模板類
- 可變數目的參數被稱爲參數包。存在兩種參數包:
- 模板參數包:表示零個或多個模板參數
- 函數參數包:表示零個或多個函數參數
- 語法格式:
- 用一個省略號來指出一個模板參數或函數參數表示一個包
- 在模板參數列表中:class...或typename...指出接下來的參數表示零個或多個類型的列表;一個類型名後面跟一個省略號表示零個多多個給定類型的非類型參數的列表
- 在函數參數列表中:如果一個參數的類型是一個模板參數包,則此參數也是一個函數參數包
演示案例
- 下面是一個可變參數模板的定義:
- 模板參數列表中:聲明一個名爲T的類型參數,和一個名爲Args的模板參數包(這個包表示零個或多個額外的類型參數)
- 函數參數列表中:聲明一個const&類型的參數,指向T的類型,還包含一個名爲reset的函數參數包(這個包表示零個或多個函數參數)
//Args是一個模板參數包;rest是一個函數參數包 //Args表示零個多多個模板類型參數 //rest表示零個或多個函數參數 template<typename T,typename... Args> void foo(const T &t, const Args& ... reset) { //... }
- 下面是一些基本的調用:
int main() { 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"); //空包 return 0; }
- 上面的四個foo()函數調用會實例化下面4個版本:
//第一個T的類型從第一個實參推斷出來,剩餘的實參從提供的額外實參中推斷出 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]&);
二、sizeof...運算符
- 當我們想要知道包中有多少元素時,可以使用sizeof...運算符,該運算符返回一個常量表達式,並且不會對其實參求值
- 下面是一個演示案例:
template<typename... Args>
void g(Args... args)
{
std::cout << sizeof...(Args) << std::endl; //類型參數的數目
std::cout << sizeof...(args) << std::endl; //函數參數的數目
}
int main()
{
int i = 0;
double d = 3.14;
string s = "how now brown cow";
g(i, s, 42, d);
g(s, 42, "hi");
g(d, s);
g("hi");
return 0;
}
template<typename T,typename... Args>
void g(const T &t,Args... args)
{
std::cout << sizeof...(Args) << std::endl;
std::cout << sizeof...(args) << std::endl;
}
int main()
{
int i = 0;
double d = 3.14;
string s = "how now brown cow";
g(i, s, 42, d);
g(s, 42, "hi");
g(d, s);
g("hi");
return 0;
}
三、編寫可變參數函數模板
- 我們曾在函數的文章中(參閱:https://blog.csdn.net/qq_41453285/article/details/91895105)介紹過普通函數的可變形參,可以使用一個initializer_list來定義一個可接受可變數目實參的函數。但是,initializer_list實參必須具有相同的類型(或者它們的類型可以莊園爲同一類型)
- 當我們既不知道想要處理的實參的數目也不知道它們的類型時,可變參數函數是很有用的
可變參數模板的遞歸調用
- 下面我們先定義一個print模板函數,它在給定一個流上打印給定實參列表的內容:
//使用參數1的輸出流,打印參數2的內容(也用來結束下面的print函數) template<typename T> ostream &print(ostream &os, const T &t) { return os << t; }
- 現在我們定義一個可變參數模板函數,也名爲print。它用來遞歸調用print函數,注意:
- 每次遞歸調用print的時候都會將Args參數的數量遞減然後傳遞給print()函數
- 當遞歸到最後Args參數只剩一個的時候就會調用上面兩個參數的print()函數(注意(重點):上面的print函數必須定義,否則下面的print函數就會永遠遞歸下去,不能夠結束)
template<typename T, typename... Args> ostream &print(ostream &os, const T &t, const Args&... rest) { os << t << ", "; //打印第一個實參 return print(os, rest...); //然後遞歸調用print }
- 假設我們調用下面的print語句,那麼遞歸會依次執行下面的結果:
int i = 10; int s = 20; print(std::cout, i, s, 42);
- 注意事項:因爲上面第一個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...); //擴展Args
}
- 對Args的擴展中,編譯器將模式const Args&應用到模板參數包Args中的每個元素。因此,此模式的擴展結果是一個逗號分隔的零個或多個類型的列表,每個類型都形如const type&,例如:
print(std::cout, i, s, 42); //包中有兩個參數
- 最後兩個實參的類型和模式一起確定了位置參數的類型。此調用被實例化爲:
print(ostream&, const int&, const string&, const int&);
- 第二個擴展發生在對print的遞歸調用中。再次情況下,模式時函數參數包的名字(即rest)。此模式擴展處一個由包中元素組成的、逗號分隔的列表。因此,這個調用等價於
print(std::cout, s, 42);
print(ostream&, const string&, const int&);
理解包擴展
- 上面的print函數參數包擴展僅僅將包擴展爲其構成元素,C++語言還允許更復雜的擴展模式
- 例如,我們編寫第二個可變參數函數errorMsg,在其內部使用print函數,然後每個實參調用debug_rep()函數打印結果:
template<typename T> string debug_rep(const T &t) { ostringstream ret; ret << t; return ret.str(); } 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)...); }
- 上面的print調用使用了模式debug_rep(rset)。此模式表示我們希望對函數參數包rest中的每個元素調用debug_rep
- 但是,下面的形式是錯誤的:
- 如果將rset...傳遞給debuf_rep()。代表debug_rep是一個可變參數模板,但是debug_rep不是
- 正確的做法是每次將一個rset參數傳遞給debug_rep
template<typename... Args> ostream &errorMsg(ostream &os, const Args&... rest) { return print(os, debug_rep(rset...)); //錯誤的 }
五、轉發參數包
- 在C++11標準下,我們可以組合使用可變參數模板與forword機制來編寫函數,實現將其實參不變地傳遞給其他函數
- forward()函數參閱:https://blog.csdn.net/qq_41453285/article/details/104447573
演示案例
- 作爲例子,我們將StrVec類添加一個emplace_back成員,標準庫容器的emplace_back成員是一個可變參數成員模板,它用其實參在容器管理的內存空間中直接構造一個元素
- 我們爲StrVec設計的emplace_back版本也應該是可變參數的,因爲string有多個構造函數,參數各不相同。由於我們希望能使用string的移動構造函數,因此還需要保持傳遞給emplace_back的實參的所有類型信息
- 因此StrVec類的emplace_back成員定義如下:
- 模板參數包擴展的模式是&&,意味着每個函數參數將是一個指向其對應實參的右值引用
class StrVec { public: template<class... Args> void emplace_back(Args&&...); };
- 其次,當emplace_back將這些實參傳遞給construct時,必須使用forward來保持實參的原始類型:
template<class... Args> void StrVec::emplace_back(Args&&... args) { chk_n_alloc(); //判斷空間是否充足 alloc.construct(first_free++,std::forward<Args>(args)...) }
- construct在first_free指向的位置中創建一個元素。construct調用中的擴展爲:
- 其中Ti表示模板參數包中第i個元素的類型,ti表示函數參數包中第i個元素
std::forward<Ti>(ti);
- 如果我們有下面的調用:
StrVec svec; svec.emplace_back(10, 'c'); //將cccccccccc添加爲新的尾元素
- construct調用中的模式會擴展出:
std::forward<int>(10),std::forward<char>(c);
- 通過在此調用中使用forward,我們保證如果一個右值調用emplace_back,則construct也會得到一個右值。例如,在下面的調用中:
svec.emplace_back(s1+s2); //使用移動構造函數
- 傳遞給emplace_back的實參是一個右值,它將以如下形式傳遞給construct:
std::forward<string>(string("the end"));
- forward<stirng>的結果類型是string&&,因此construct將得到一個右值引用實參,construct會繼續將此實參傳遞給string的移動構造函數來創建新元素