可變參數模板
原文鏈接: http://blog.csdn.net/xiaohu2022/article/details/69076281
https://www.cnblogs.com/qicosmos/p/4325949.html
普通模板只可以采取固定數量的模板參數。然而,有時候我們希望模板可以接收任意數量的模板參數,這個時候可以采用可變參數模板。對於可變參數模板,其將包含至少一個模板參數包,模板參數包是可以接收0個或者多個參數的模板參數。相應地,存在函數參數包,意味着這個函數參數可以接收任意數量的參數。
可變模板參數---- C++11新特性
- 可變模板參數(variadic templates)是C++11新增的最強大的特性之一,它對參數進行了高度泛化,它能表示0到任意個數、任意類型的參數
- 由於可變模版參數比較抽象,使用起來需要一定的技巧,所以它也是C++11中最難理解和掌握的特性之一
參數包(parameter pack)
模板參數包,如:
template<typename… Args>class tuple;
- Args標識符的左側使用了省略號,在C++11中Args被稱為“模板參數包”,表示可以接受任意多個參數作為模板參數,編譯器將多個模板參數打包成“單個”的模板參數包.
函數參數包,如
template<typename…T> void f(T…args);
- args被稱為函數參數包,表示函數可以接受多個任意類型的參數.
在C++11標准中,要求函數參數包必須唯一,且是函數的最后一個參數; 模板參數包則沒有
當聲明一個變量(或標識符)為可變參數時,省略號位於該變量的左側
當使用參數包時,省略號位於參數名稱的右側,表示立即展開該參數,這個過程也被稱為解包
包擴展表達式
設args被聲明為一個函數參數包,其擴展方式有
printArgs(args…)
- 相當於printArgs(args1,args2,…,argsN)
printArgs(args)…
- 相當於printArgs(args1),…, printArgs(argsN)
(printArgs(args),0)… 逗號表達式
- 這是一個逗號表達式。相當於(printArgs(args1),0),…(printArgs(argsN),0)
包擴展表達式“exp…”相當於將省略號左側的參數包exp視為一個整體來進行擴展
使用規則
可變參數模板和普通模板的語義是一樣的,只是寫法上稍有區別,聲明可變參數模板時需要在typename或class后面帶上省略號“...”。比如我們常常這樣聲明一個可變模版參數:template<typename...>或者template<class...>,一個典型的可變模版參數的定義是這樣的:
template <class... T> void f(T... args);
上面的可變模版參數的定義當中,省略號的作用有兩個:
1.聲明一個參數包T... args,這個參數包中可以包含0到任意個模板參數;
2.在模板定義的右邊,可以將參數包展開成一個一個獨立的參數。
上面的參數args前面有省略號,所以它就是一個可變模版參數,我們把帶省略號的參數稱為“參數包”,它里面包含了0到N(N>=0)個模版參數。我們無法直接獲取參數包args中的每個參數的,只能通過展開參數包的方式來獲取參數包中的每個參數,這是使用可變模版參數的一個主要特點,也是最大的難點,即如何展開可變模版參數。
可變模版參數和普通的模版參數語義是一致的,所以可以應用於函數和類,即可變模版參數函數和可變模版參數類,然而,模版函數不支持偏特化,所以可變模版參數函數和可變模版參數類展開可變模版參數的方法還不盡相同,下面我們來分別看看他們展開可變模版參數的方法。
一個可變參數tuple類模板定義如下:
template<typename ... Types> class Tuple {};
可以用任意數量的類型來實例化Tuple:
Tuple<> t0;
Tuple<int> t1; Tuple<int, string> t2; // Tuple<0> error; 0 is not a type
如果想避免出現用0個模板參數來實例化可變參數模板,可以這樣定義模板:
template<typename T, typename ... Types> class Tuple {};
此時在實例化時,必須傳入至少一個模板參數,否則無法編譯。
同樣地,可以定義接收任意參數的可變參數函數模板:
template<typename ... Types> void f(Types ... args); // 一些合法的調用 f(); f(1); f(3.4, "hello");
對於類模板來說,可變模板參數包必須是模板參數列表中的最后一個參數。但是對於函數模板來說,則沒有這個限制,考慮下面的情況:
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}
可變參數函數模板實例
一個簡單的可變模版參數函數:
template <class... T> void f(T... args) { cout << sizeof...(args) << endl; //打印變參的個數 } f(); //0 f(1, 2); //2 f(1, 2.5, ""); //3
上面的例子中,f()沒有傳入參數,所以參數包為空,輸出的size為0,后面兩次調用分別傳入兩個和三個參數,故輸出的size分別為2和3。由於可變模版參數的類型和個數是不固定的,所以我們可以傳任意類型和個數的參數給函數f。這個例子只是簡單的將可變模版參數的個數打印出來,如果我們需要將參數包中的每個參數打印出來的話就需要通過一些方法了。展開可變模版參數函數的方法一般有兩種:一種是通過遞歸函數來展開參數包,另外一種是通過逗號表達式來展開參數包。下面來看看如何用這兩種方法來展開參數包。
2.1.1遞歸函數方式展開參數包
無法直接遍歷傳給可變參數模板的不同參數,但是可以借助遞歸的方式來使用可變參數模板。可變參數模板允許創建類型安全的可變長度參數列表。下面定義一個可變參數函數模板processValues(),它允許以類型安全的方式接受不同類型的可變數目的參數。函數processValues()會處理可變參數列表中的每個值,對每個參數執行對應版本的handleValue()。
// 處理每個類型的實際函數 void handleValue(int value) { cout << "Integer: " << value << endl; } void handleValue(double value) { cout << "Double: " << value << endl; } void handleValue(string value) { cout << "String: " << value << endl; } // 用於終止迭代的基函數 template<typename T> void processValues(T arg) { handleValue(arg); } // 可變參數函數模板 template<typename T, typename ... Ts> void processValues(T arg, Ts ... args) { handleValue(arg); processValues(args ...); // 解包,然后遞歸 }
可以看到這個例子用了三次... 運算符,但是有兩層不同的含義。用在參數模板列表以及函數參數列表,其表示的是參數包。前面說到,參數包可以接受任意數量的參數。用在函數實際調用中的...運算符,它表示參數包擴展,此時會對args解包,展開各個參數,並用逗號分隔。模板總是至少需要一個參數,通過args...解包可以遞歸調用processValues(),這樣每次調用都會至少用到一個模板參數。對於遞歸來說,需要終止條件,當解包后的參數只有一個時,調用接收一個參數模板的processValues()函數,從而終止整個遞歸。
假如對processValues()進行如下調用:
processsValues(1, 2.5, "test");
其產生的遞歸調用如下:
processsValues(1, 2.5, "test"); handleValue(1); processsValues(2.5, "test"); handleValue(2.5); processsValues("test"); handleValue("test");
由於processValues()函數會根據實際類型推導自動調用正確版本的handleValue()函數,所以這種可變參數列表是完全類型安全的。如果調用processValues()函數帶有的一個參數,無對應的handleValue()函數版本,那么編譯器會產生一個錯誤。
前面的實現有一個致命的缺陷,那就是遞歸調用時參數是復制傳值的,對於有些類型參數,其代價可能會很高。一個高效且合理的方式是按引用傳值,但是對於字面量調用processValues()這樣會存在問題,因為字面量僅允許傳給const引用參數。比較幸運的是,我們可以考慮右值引用。使用std::forward()函數可以實現這樣的處理,當把右值引用傳遞給processValues()函數時,它就傳遞為右值引用,但是如果把左值引用傳遞給processValues()函數時,它就傳遞為左值引用。下面是具體實現:
// 用於終止迭代的基函數 template<typename T> void processValues(T &&arg) { handleValue(std::forward<T>(arg)); } // 可變參數函數模板 template<typename T, typename ... Ts> void processValues(T&& arg, Ts&& ... args) { handleValue(std::forward<T>(arg)); processValues(std::forward<Ts>(args) ...); // 先使用forward函數處理后,再解包,然后遞歸 }
實現簡化的printf函數
這里我們通過可變參數模板實現一個簡化版本的printf函數:
// 基函數 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的第一個參數固定是const char*類型。
2.1.2逗號表達式展開參數包
遞歸函數展開參數包是一種標准做法,也比較好理解,但也有一個缺點,就是必須要一個重載的遞歸終止函數,即必須要有一個同名的終止函數來終止遞歸,這樣可能會感覺稍有不便。有沒有一種更簡單的方式呢?其實還有一種方法可以不通過遞歸方式來展開參數包,這種方式需要借助逗號表達式和初始化列表。比如前面print的例子可以改成這樣:
template <class T> void printarg(T t) { cout << t << endl; } template <class ...Args> void expand(Args... args) { int arr[] = {(printarg(args), 0)...}; } expand(1,2,3,4);
References
[1] Marc Gregoire. Professional C++, Third Edition, 2016.
[2] cppreference parameter pack