可變參數(shù)模板
原文鏈接: http://blog.csdn.net/xiaohu2022/article/details/69076281
普通模板只可以采取固定數(shù)量的模板參數(shù)。然而,有時候我們希望模板可以接收任意數(shù)量的模板參數(shù),這個時候可以采用可變參數(shù)模板。對于可變參數(shù)模板,其將包含至少一個模板參數(shù)包,模板參數(shù)包是可以接收0個或者多個參數(shù)的模板參數(shù)。相應(yīng)地,存在函數(shù)參數(shù)包,意味著這個函數(shù)參數(shù)可以接收任意數(shù)量的參數(shù)。
使用規(guī)則
一個可變參數(shù)類模板定義如下:
template<typename ... Types>
class Tuple
{};
可以用任意數(shù)量的類型來實例化Tuple:
Tuple<> t0;
Tuple<int> t1;
Tuple<int, string> t2;
// Tuple<0> error; 0 is not a type
如果想避免出現(xiàn)用0個模板參數(shù)來實例化可變參數(shù)模板,可以這樣定義模板:
template<typename T, typename ... Types>
class Tuple
{};
此時在實例化時,必須傳入至少一個模板參數(shù),否則無法編譯。
同樣地,可以定義接收任意參數(shù)的可變參數(shù)函數(shù)模板:
template<typename ... Types>
void f(Types ... args);
// 一些合法的調(diào)用
f();
f(1);
f(3.4, "hello");
對于類模板來說,可變模板參數(shù)包必須是模板參數(shù)列表中的最后一個參數(shù)。但是對于函數(shù)模板來說,則沒有這個限制,考慮下面的情況:
template<typename ... Ts, typename U>
class Invalid
{}; // 這是非法的定義,因為永遠無法推斷出U的類型
template<typename ... Ts, typename U>
void valid(U u, Ts ... args); // 這是合法的,因為可以推斷出U的類型
// void invalid(Ts ... args, U u); // 非法的,永遠無法推斷出U
valid(1.0, 1, 2, 3); // 此時,U的類型是double,Ts是{int, int, int}
可變參數(shù)函數(shù)模板實例
無法直接遍歷傳給可變參數(shù)模板的不同參數(shù),但是可以借助遞歸的方式來使用可變參數(shù)模板。可變參數(shù)模板允許創(chuàng)建類型安全的可變長度參數(shù)列表。下面定義一個可變參數(shù)函數(shù)模板processValues(),它允許以類型安全的方式接受不同類型的可變數(shù)目的參數(shù)。函數(shù)processValues()會處理可變參數(shù)列表中的每個值,對每個參數(shù)執(zhí)行對應(yīng)版本的handleValue()。
// 處理每個類型的實際函數(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 ...); // 解包,然后遞歸
}
可以看到這個例子用了三次... 運算符,但是有兩層不同的含義。用在參數(shù)模板列表以及函數(shù)參數(shù)列表,其表示的是參數(shù)包。前面說到,參數(shù)包可以接受任意數(shù)量的參數(shù)。用在函數(shù)實際調(diào)用中的...運算符,它表示參數(shù)包擴展,此時會對args解包,展開各個參數(shù),并用逗號分隔。模板總是至少需要一個參數(shù),通過args...解包可以遞歸調(diào)用processValues(),這樣每次調(diào)用都會至少用到一個模板參數(shù)。對于遞歸來說,需要終止條件,當(dāng)解包后的參數(shù)只有一個時,調(diào)用接收一個參數(shù)模板的processValues()函數(shù),從而終止整個遞歸。
假如對processValues()進行如下調(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ù)會根據(jù)實際類型推導(dǎo)自動調(diào)用正確版本的handleValue()函數(shù),所以這種可變參數(shù)列表是完全類型安全的。如果調(diào)用processValues()函數(shù)帶有的一個參數(shù),無對應(yīng)的handleValue()函數(shù)版本,那么編譯器會產(chǎn)生一個錯誤。
前面的實現(xiàn)有一個致命的缺陷,那就是遞歸調(diào)用時參數(shù)是復(fù)制傳值的,對于有些類型參數(shù),其代價可能會很高。一個高效且合理的方式是按引用傳值,但是對于字面量調(diào)用processValues()這樣會存在問題,因為字面量僅允許傳給const引用參數(shù)。比較幸運的是,我們可以考慮右值引用。使用std::forward()函數(shù)可以實現(xiàn)這樣的處理,當(dāng)把右值引用傳遞給processValues()函數(shù)時,它就傳遞為右值引用,但是如果把左值引用傳遞給processValues()函數(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ù)處理后,再解包,然后遞歸
}
實現(xiàn)簡化的printf函數(shù)
這里我們通過可變參數(shù)模板實現(xià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的第一個參數(shù)固定是const char*類型。
References
[1] Marc Gregoire. Professional C++, Third Edition, 2016.
[2] cppreference parameter pack