如果引數的個數無法事先確定,而且引數的型態可能各不相同,C++ 11 以後可以透過可變參數模版(variadic template)來解決。
#include <iostream>
using namespace std;
template <typename... Types>
void foo(Types... params) {
cout << sizeof...(Types) << " "
<< sizeof...(params) << endl;
}
int main() {
foo(1); // 顯示 1 1
foo(1, "XD"); // 顯示 2 2
foo(1, "XD", 3.14); // 顯示 3 3
return 0;
}
typename
之後接續了省略語法 ...
,這可以看成 Types
代表不定長度的 typename T1, typename T2, ...
,Types
被稱為模版參數包(template parameter pack),宣告參數時,Types... params
代表了不定長度的 T1 t1, T2 t2, ...
,params
被稱為函式參數包(function parameter pack)。
可以使用 sizeof...
來得知實際呼叫時的型態數量或引數數量,這個值是編譯時期推斷得知的,根據範例中呼叫的方式,在編譯時期 foo
會被實例出 foo(int)
、foo(int, const char*)
與 foo(int, const char*, double)
版本,因此 params
並不是個物件,那麼該怎麼取得呼叫時的引數呢?
如果呼叫時的引數是同一型態,一個簡單的方式是展開為陣列、vector
等型態。例如:
#include <iostream>
#include <vector>
using namespace std;
template <typename T, typename ...Ts>
T sum(T first, Ts... rest) {
vector<T> nums = {rest...};
T r = first;
for(auto n : nums) {
r += n;
}
return r;
}
int main() {
cout << sum(1, 2, 3) << endl;
cout << sum(1, 2, 3, 4, 5) << endl;
return 0;
}
在編譯時期,上面的範例會產生 sum(int, int, int)
與 sum(int, int, int, int, int)
兩個版本,而 {rest...}
用來解開參數包,解開之意是指 {rest...}
會分別產生 {p1, p2, p3}
與 {p1, p2, p3, p4, p5}
(p1
等名稱代表參數)。
如果實際上傳遞的引數型態各不相同,又該怎麼辦,這時得使用遞迴並配合解開參數包。例如:
#include <iostream>
using namespace std;
template <typename T>
void print(T p) {
cout << p << endl;
}
template <typename T, typename ...Ts>
void print(T first, Ts... rest) {
cout << first << " ";
print(rest...);
}
int main() {
print(1);
print(1, "2");
print(1, "2", 3.14);
return 0;
}
print(1)
會實例一個 print(int)
版本,這沒有問題;print(1, "2")
的實例一個 print(int, const char*)
版本,然後 print(rest..)
的部份會解開為 print("2")
,這又會實例出 print(const char*)
:
void print(const char* p) {
cout << p << endl;
}
void print(int p1, const char* p2) {
cout << p1 << " ";
print(p2);
}
這就是為何可變參數模版可以接受不同型態引數的原因了,依以上的說明,print(1, "2", 3.14)
最後會實例出以下的版本:
void print(double p) {
cout << p << endl;
}
void print(const char* p1, double p2) {
cout << p1 << " ";
print(p3);
}
void print(int p1, const char* p2, double p3) {
cout << p1 << " ";
print(p2, p3);
}
在〈Parameter pack〉中就舉了個實作 tprintf
函式的例子:
#include <iostream>
void tprintf(const char* format) // base function
{
std::cout << format;
}
template<typename T, typename... Targs>
void tprintf(const char* format, T value, Targs... Fargs) // recursive variadic function
{
for ( ; *format != '\0'; format++ ) {
if ( *format == '%' ) {
std::cout << value;
tprintf(format+1, Fargs...); // recursive call
return;
}
std::cout << *format;
}
}
int main()
{
tprintf("% world% %\n","Hello",'!',123);
return 0;
}
因為可變參數模版可以使用的引數會是各種型態,若各種不同型態有各自的處理方式,那就得知道目前處理的資料是哪種型態,而以上的例子也暗示了,可以像 C 的 printf
,在格式上指定 %d
、%s
等,提供資訊以進一步決定各個引數的型態為何,以進行相對應的處理。
在解開參數包的同時,可以指定套用某個函式,例如:
#include <iostream>
#include <vector>
using namespace std;
template<typename T>
T sum(T first) {
return first;
}
template<typename T, typename... Ts>
T sum(T first, Ts... params) {
return first + sum(params...);
}
template<typename T>
T doubleIt(T t) {
return t + t;
}
template<typename T, typename... Ts>
T doubleSum(T first, Ts... params) {
return doubleIt(first) + sum(doubleIt(params)...);
}
int main() {
cout << sum(1, 2) << endl; // 3
cout << sum(string("1"), string("2")) << endl; // 12
cout << doubleSum(1, 2) << endl; // 6
cout << doubleSum(string("1"), string("2")) << endl; // 1212
return 0;
}
在 doubleSum
中的 sum(doubleIt(params)...)
,指的是解開參數包的時候,用每個參數呼叫 doubleIt
,如果是 doubleSum(string("1"), string("2"), string("3"))
的呼叫,編譯器產生的版本會是(至於其他版本就依此類推了):
string doubleSum(string first, string p1, string p2) {
return doubleIt(first) + sum(doubleIt(p1), doubleIt(p2));
}