可变参数模版


如果实参的个数无法事先确定,而且实参的类型可能各不相同,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));
}




展开阅读全文