可變參數(shù)模板
原文鏈接: http://blog.csdn.net/xiaohu2022/article/details/69076281
普通模板只可以采取固定數(shù)量的模板參數(shù)形葬。然而裳凸,有時(shí)候我們希望模板可以接收任意數(shù)量的模板參數(shù)禀崖,這個(gè)時(shí)候可以采用可變參數(shù)模板施敢。對(duì)于可變參數(shù)模板牲证,其將包含至少一個(gè)模板參數(shù)包,模板參數(shù)包是可以接收0個(gè)或者多個(gè)參數(shù)的模板參數(shù)。相應(yīng)地,存在函數(shù)參數(shù)包入桂,意味著這個(gè)函數(shù)參數(shù)可以接收任意數(shù)量的參數(shù)。
使用規(guī)則
一個(gè)可變參數(shù)類模板定義如下:
template<typename ... Types>
class Tuple
{};
可以用任意數(shù)量的類型來(lái)實(shí)例化Tuple:
Tuple<> t0;
Tuple<int> t1;
Tuple<int, string> t2;
// Tuple<0> error; 0 is not a type
如果想避免出現(xiàn)用0個(gè)模板參數(shù)來(lái)實(shí)例化可變參數(shù)模板驳阎,可以這樣定義模板:
template<typename T, typename ... Types>
class Tuple
{};
此時(shí)在實(shí)例化時(shí)抗愁,必須傳入至少一個(gè)模板參數(shù)馁蒂,否則無(wú)法編譯。
同樣地蜘腌,可以定義接收任意參數(shù)的可變參數(shù)函數(shù)模板:
template<typename ... Types>
void f(Types ... args);
// 一些合法的調(diào)用
f();
f(1);
f(3.4, "hello");
對(duì)于類模板來(lái)說(shuō)沫屡,可變模板參數(shù)包必須是模板參數(shù)列表中的最后一個(gè)參數(shù)。但是對(duì)于函數(shù)模板來(lái)說(shuō)逢捺,則沒(méi)有這個(gè)限制谁鳍,考慮下面的情況:
template<typename ... Ts, typename U>
class Invalid
{}; // 這是非法的定義,因?yàn)橛肋h(yuǎn)無(wú)法推斷出U的類型
template<typename ... Ts, typename U>
void valid(U u, Ts ... args); // 這是合法的劫瞳,因?yàn)榭梢酝茢喑鯱的類型
// void invalid(Ts ... args, U u); // 非法的,永遠(yuǎn)無(wú)法推斷出U
valid(1.0, 1, 2, 3); // 此時(shí)绷柒,U的類型是double志于,Ts是{int, int, int}
可變參數(shù)函數(shù)模板實(shí)例
無(wú)法直接遍歷傳給可變參數(shù)模板的不同參數(shù),但是可以借助遞歸的方式來(lái)使用可變參數(shù)模板废睦∷耪溃可變參數(shù)模板允許創(chuàng)建類型安全的可變長(zhǎng)度參數(shù)列表。下面定義一個(gè)可變參數(shù)函數(shù)模板processValues()嗜湃,它允許以類型安全的方式接受不同類型的可變數(shù)目的參數(shù)奈应。函數(shù)processValues()會(huì)處理可變參數(shù)列表中的每個(gè)值,對(duì)每個(gè)參數(shù)執(zhí)行對(duì)應(yīng)版本的handleValue()购披。
// 處理每個(gè)類型的實(shí)際函數(shù)
void handleValue(int value) { cout << "Integer: " << value << endl; }
void handleValue(double value) { cout << "Double: " << value << endl; }
void handleValue(string value) { cout << "String: " << value << endl; }
// 用于終止迭代的基函數(shù)
template<typename T>
void processValues(T arg)
{
handleValue(arg);
}
// 可變參數(shù)函數(shù)模板
template<typename T, typename ... Ts>
void processValues(T arg, Ts ... args)
{
handleValue(arg);
processValues(args ...); // 解包杖挣,然后遞歸
}
可以看到這個(gè)例子用了三次... 運(yùn)算符,但是有兩層不同的含義刚陡。用在參數(shù)模板列表以及函數(shù)參數(shù)列表惩妇,其表示的是參數(shù)包。前面說(shuō)到筐乳,參數(shù)包可以接受任意數(shù)量的參數(shù)歌殃。用在函數(shù)實(shí)際調(diào)用中的...運(yùn)算符,它表示參數(shù)包擴(kuò)展蝙云,此時(shí)會(huì)對(duì)args解包氓皱,展開(kāi)各個(gè)參數(shù),并用逗號(hào)分隔勃刨。模板總是至少需要一個(gè)參數(shù)波材,通過(guò)args...解包可以遞歸調(diào)用processValues(),這樣每次調(diào)用都會(huì)至少用到一個(gè)模板參數(shù)朵你。對(duì)于遞歸來(lái)說(shuō)各聘,需要終止條件,當(dāng)解包后的參數(shù)只有一個(gè)時(shí)抡医,調(diào)用接收一個(gè)參數(shù)模板的processValues()函數(shù)躲因,從而終止整個(gè)遞歸早敬。
假如對(duì)processValues()進(jìn)行如下調(diào)用:
processsValues(1, 2.5, "test");
其產(chǎn)生的遞歸調(diào)用如下:
processsValues(1, 2.5, "test");
handleValue(1);
processsValues(2.5, "test");
handleValue(2.5);
processsValues("test");
handleValue("test");
由于processValues()函數(shù)會(huì)根據(jù)實(shí)際類型推導(dǎo)自動(dòng)調(diào)用正確版本的handleValue()函數(shù),所以這種可變參數(shù)列表是完全類型安全的大脉。如果調(diào)用processValues()函數(shù)帶有的一個(gè)參數(shù)搞监,無(wú)對(duì)應(yīng)的handleValue()函數(shù)版本,那么編譯器會(huì)產(chǎn)生一個(gè)錯(cuò)誤镰矿。
前面的實(shí)現(xiàn)有一個(gè)致命的缺陷琐驴,那就是遞歸調(diào)用時(shí)參數(shù)是復(fù)制傳值的,對(duì)于有些類型參數(shù)秤标,其代價(jià)可能會(huì)很高绝淡。一個(gè)高效且合理的方式是按引用傳值,但是對(duì)于字面量調(diào)用processValues()這樣會(huì)存在問(wèn)題苍姜,因?yàn)樽置媪績(jī)H允許傳給const引用參數(shù)牢酵。比較幸運(yùn)的是,我們可以考慮右值引用衙猪。使用std::forward()函數(shù)可以實(shí)現(xiàn)這樣的處理馍乙,當(dāng)把右值引用傳遞給processValues()函數(shù)時(shí),它就傳遞為右值引用垫释,但是如果把左值引用傳遞給processValues()函數(shù)時(shí)丝格,它就傳遞為左值引用。下面是具體實(shí)現(xiàn):
// 用于終止迭代的基函數(shù)
template<typename T>
void processValues(T &&arg)
{
handleValue(std::forward<T>(arg));
}
// 可變參數(shù)函數(shù)模板
template<typename T, typename ... Ts>
void processValues(T&& arg, Ts&& ... args)
{
handleValue(std::forward<T>(arg));
processValues(std::forward<Ts>(args) ...); // 先使用forward函數(shù)處理后棵譬,再解包显蝌,然后遞歸
}
實(shí)現(xiàn)簡(jiǎn)化的printf函數(shù)
這里我們通過(guò)可變參數(shù)模板實(shí)現(xiàn)一個(gè)簡(jiǎn)化版本的printf函數(shù):
// 基函數(shù)
void tprintf(const char* format)
{
cout << format;
}
template<typename T, typename ... Ts>
void tprintf(const char* format, T&& value, Ts&& ... args)
{
for (; *format != '\0'; ++format)
{
if (*format == '%')
{
cout << value;
tprintf(format + 1, std::forward<Ts>(args) ...); // 遞歸
return;
}
cout << *format;
}
}
int main()
{
tprintf("% world% %\n", "Hello", '!', 2017);
// output: Hello, world! 2017
cin.ignore(10);
return 0;
}
其方法基本與processValues()是一致的,但是由于tprintf的第一個(gè)參數(shù)固定是const char*類型茫船。
References
[1] Marc Gregoire. Professional C++, Third Edition, 2016.
[2] cppreference parameter pack