C++ 变长模板 template<typename... T>

变长函数

C中变长参数

变长函数(variadic function)是指函数参数个数可变。在C中,有两种方式表示可变参数:1)使用"..."表示可变参数,如printf;2)使用变长宏va_list表示可变参数,如vprintf。关于va_list,可参见linux C 可变参数类型va_list

#incldue <stdio.h>
int printf(const char *format, ...);

#include <stdarg.h>
int vprintf(const char *format, va_list ap);

变长参数"..."可以通过一组"va_"开头的宏,转换成va_list,从而实现逐个参数解析。

例如,一个变长函数SumOfFloat可以这样编写

#include <stdio.h>
#include <stdarg.h>

// 变长函数求和
double SumOfFloat(int count, ...) {
    va_list ap;
    double sum = 0;
    va_start(ap, fmt); // 获得变长列表的句柄ap
    for (int i = 0; i < count; i++) {
        sum += va_arg(ap, double); // 每次获得一个参数
    }
    va_end(ap);
    return sum;
}

int main() {
    printf("%f\n", SumOfFloat(3, 1.2f, 3.4, 5.6)); // 10.200000
}

va_arg从ap中,将参数一一取出用于运算,这是得益于传递字符串中形如"%s", "%d"这样的转义字符,以及count参数,才知道每个参数类型(可确定长度),参数的个数。

C++中变长参数

有些没有定义转义字符的非POD数据,比如std::string,使用变长函数就会导致未定义行为。例如,下面代码会导致printf出错:

const char *msg = "hello %s";
printf(msg, std::string("world")); // error: 因为没有定义std::string的转义字符

那么C++中,是如何实现变长参数的呢?
一个很好的办法是使用C++ 函数模板。C++11引入参数个数可变的模板,下面以tuple(多元组)为例进行讲解。

例如,可通过以下方式,来使用类模板tuple:

std::tuple<double, char, std::string> collections; // 声明一个tuple模板类, 可容纳三种类型的数据

std::make_tuple(9.8, 'g', "gravity");  // 创建一个tuple模板类型

实际上,tuple包含的类型数量可以任意多,这是通过变长模板(variadic template)来实现的。


变长模板

变长模板类

变长模板语法,以tuple为例

template<typename... Elements> class tuple;

Elements前面三个点"...",表示该参数是变长的。C++11中,Elements被称为“模板参数包”(template parameter pack)。有了这样的参数包,类模板tuple就能接受任意多个参数作为模板参数。

1)使用普通类型对类模板进行实例化

tuple<int, char, double>

实例化后,模板参数包Elements就是一个包含int, char, double三种类型的集合。

2)使用非类型的字面量实例化类模板

template<int... A> class NonTypeVariadicTemplate{};
NonTypeVariadicTemplate<1, 0, 2> ntvr; // 注意这类模板参数是字面量, 而非类型

// 上面代码相当于
template<int, int, int> class NonTypeVariadicTemplate{};
NonTypeVariadicTemplate<1, 0, 2> ntvr;

模板推导时,如何解析模板参数包呢?
一个模板参数包在模板推导时,会被认为是模板的单个参数。为使用模板参数包,我们需要对其进行解包(unpack)。C++11中,通过包扩展(pack expansion)表达式来完成解包。

template<typename T1, typename T2> class B{};
template<typename... A> class Template : private B<A...>{}; // 模板参数包A, 而包扩展A...在是Template私有基类B<A...>中
Template<X, Y> xy; // xy是Template<X, y>类对象

上面代码中,X、Y两个模板参数先被打包为参数包A,而后在包扩展A...中被还原。但如果声明Template<X, Y, Z>对象,就会导致模板推导错误,从而无法支持多个模板参数。

实际应用中, 通常通过递归+偏特化来实现任意多个模板参数。定义递归的模板偏特化,使得模板参数包在实例化时能层层展开,直到参数包中的参数逐渐耗尽或者到达某个数量边界为止。

例如,使用递推结构实现变长模板

// 模板参数是类型

template<typename... Elements> class tuple; // 变长模板的声明

template<typename Head, typename...Tail> // 递归的偏特化定义
class tuple<Head, Tail...> : private tuple<Tail...> {
    Head head;
};

template<> class tuple<>{};  // 偏特化定义边界条件

注意:偏特化不仅针对模板参数的类型,还能针对模板参数的个数。关键是偏特化版本的模板参数是原模板参数子集。

下面是一个使用非类型模板的例子,使用递归结构实现的变长模板函数,从而实现变长参数的乘法

// 模板参数是非类型(字面量)

#include <iostream>
using namespace std;

template<long... nums> struct Multiply; // 变长模板的声明. 注意这类参数是非类型

template<long first, long... last>
struct Multiply<first, last...> { // 递归的偏特化定义.
    static const long val = first * Multiply<last...>::val;
};

template<>
struct Multiply<> { // 偏特化定义边界条件
    static const long val = 1;
};

int main() {
    cout << Multiply<2, 3, 4, 5>::val << endl; // 120
    cout << Multiply<22, 44, 66, 88, 9>::val << endl; // 50599296
}

变长模板函数

除变长模板类,C++11也支持声明变长模板函数。除了模板参数可以声明为容纳变长的模板参数包,相应的变长函数参数也可以声明为函数参数包。如:

template<typename... T> void f(T ... args);

T是变长模板参数(类型),即模板参数包;arg是对应于这些变长类型的数据,即函数参数包。C++11要求,函数参数包必须唯一,而且是函数的最后一个参数(模板参数包无此要求)。

有了模板参数包和函数参数包,我们可以实现类似于C中的变长函数功能了。

#include <iostream>
#include <stdexcept>
using namespace std;

void Printf(const char* s) { // 边界条件
    while (*s) {
        if (*s == '%' && *++s != '%') 
            throw runtime_error("invalid format string: missing arguments");
        cout << *s++;
    }
}

template<typename T, typename... Args>
void Printf(const char* s, T value, Args... args) {
    while (*s) {
        // 处理转义字符
        if (*s == '%' && *++s != '%') { // 出现 %c, c是非'%'字符, 则继续递归
            cout << value;
            return Printf(++s, args...);
        }
        count << *s++;
    }
    throw runtime_error("extra arguments provided to Printf");
}

int main() {
    Printf("hello %s\n", std::string("world")); // hello world
}

变长函数模板不会丢弃参数的类型信息,重载的cout的操作符<<总可以将具有类型的变量正确地打印出来。这是变长模板函数Printf功能大于变长函数的地方。


变长模板高级功能

特殊的包扩展方式

C++标准定义了7种参数包可以展开的位置:
1)表达式
2)初始化列表
3)基类描述符列表
4)类成员初始化列表
5)模板参数列表
6)通用属性列表
7)lambda函数的捕捉列表

而在其他“地方”,则无法展开参数包。
特殊包扩展表达式:

  • 当声明Arg为参数包,那么可以用Arg&&...这样的包扩展表达式,解包后等价于Arg1&&, ..., Argn&&(假设Arg1是参数包第一个参数,Argn是最后一个)。
  • "..."位于模板参数列表,和位于模板参数列表外
template<typename... A> class T : private B<A>...{}; // case 1

template<typename... A> class T : private B<A...>{}; // case 2

这2种包扩展表达式,解包后,对于同样的实例化T<X, Y>,解包是不一样的

class T<X, Y> : private B<X>, private B<Y>{}; // case 1解包后, T<X,Y>多次继承的派生类

class T<X, Y> : private B<X, Y>{};            // case 2解包后, T<X, Y>是多参数的模板类的派生类

函数声明也会有类似情况:

#include <iostream>
using namespace std;

template<typename... T> void DummyWrapper(T... t) {}

template<typename T> T pr(T t) {
    cout << t;
    return t;
}

template<typename... A>
void VTPrint(A... a) {
    DummpyWrapper(pr(a)...); // 注意"..."的位置, 位于pr函数参数列表外, 用于扩展"pr()"; 包扩展解包为pr(1), pr(", "), pr(1.2), pr(", abc\n")
}

int main() {
    VTPrint(1, ", ", 1.2, ", abc\n");
}

"sizeof..."计算参数包参数个数

程序如何知道参数包中的参数个数呢?
C++11引入新操作费"sizeof...",可用于计算参数包中参数个数。

注意:sizeof... 的操作对象是参数包。

template<typename... A> int Vaarge(A... args) {
    int size = sizeof...(A); // 计算变长参数包的长度
    cout << size << endl;
        ...
}

模板参数包是模板最后一个参数吗?

模板参数包不一定非得作为(类)模板的最后一个参数。比如下面的例子,模板参数包TArgs并非模板的最后一个参数

#include <cstdio>
#include <tuple>
using namespace std;

// 偏特化定义 边界条件
template<typename A, typename B> struct S {};

// 4个模板参数, T和U是变长模板类, TArgs和UArgs是模板参数包
// 可见, 模板参数包TArgs并非模板的最后一个参数
template<
        template<typename...> class T, typename... TArgs,
        template<typename...> class U, typename... UArgs
        >
        struct S< T< TArgs...>, U< UArgs...>> {};

int main(int argc, char* argv[])
{
// HelloFunc();
    S<int, float> p;
    S<std::tuple<int, char>, std::tuple<float>> s;
   return 0;
}

小结

包扩展符"..."紧跟着什么,解包后,扩展的就是什么。比如,"pr(a)...",扩展的是函数调用pr(a)本身;而"pr(a...)"扩展的是函数参数。

参考

[1]MichaelWon, IBM XL编译器中国开发团队. 深入理解C++11:C++11新特性解析与应用[M]. 机械工业出版社, 2013.

posted @ 2022-09-29 22:39  明明1109  阅读(1688)  评论(0编辑  收藏  举报