6.2 C++11变长模板

一、引入

在(C++11)之前,我们可以通过C语言提供的变长函数实现一个可以接受任意长度参数列表的求和函数:

double sum(int count,...)
{
	va_list ap;
	va_start(ap, count);
	double sum = 0;
	for (int i = 0; i < count; i++)
		sum += va_arg(ap, double);

	va_end(ap);
	return sum;
}
  • va_list声明了一个指针ap
  • va_start初始化了变量ap
  • va_arg则是按照指定类型读取参数
  • va_end表示参数读取结束,释放ap

上面的例子虽然实现了可变长参数的函数,但是明显有着不便之处:

  • 在函数内指定参数类型,不够友好
  • 只能接收单一类型的参数
  • 需要指定参数长度(参数count)

也就是说,实际上C语言提供的可变长函数,并没有实现完全意义上的变长参数。

而在C++11开始,引入了变长模板的概念,可以以此实现真正的变长参数。

二、变长模板:模板参数包和函数参数包

根据变长模板应用于类模板还是函数模板分为模板参数包和函数参数包。

1.模板参数包

类型模板参数包

即模板参数包是带类型的,可以是相同类型,也可以是不同类型。

C++11变长模板的语法如下:

template  class tuple;

以typename后的...表示该参数是变长的。其中Elements称为“模板参数包”,是一种新的模板参数类型。

而对于下面的实例化:

tuple

编译器进行类型推导时,会将int,char,double三个参数打包成一个模板参数包Elements。

非类型模板参数包

有时我们可以确定所有类型都是相同的,那么可以指定模板参数包的参数类型,从而获得非类型的模板参数包:

template class NonTypeVariadicTemplate{};
NonTypeVariadicTemplate<1, 0, 2> ntvt;

这里A就包含了1,0,2三个值,而非类型。

除此之外还有模板类型的模板参数包,这个比较复杂,在后面展示。

解包与包扩展

一个模板参数包在模板推导时会被认为是模板的单个参数(虽然实际上它将会打包任意数量的实参)。为了使用模板参数包,我们总是需要将其解包(unpack)。在C++11中,这通常是通过一个名为包扩展(pack expansion)的表达式来完成,比如:

template class Template: private B{};

其中A...就是一个包扩展。更具体的如下:

template class B{};
template class Template: private B{};
Template xy;

第三句Template将调用第二句的变长模板Template,然后调用其基类B,并进行解包,将参数X,Y传给B。当然,如果包扩展的的参数(数目)不符合,则会报错。

那么我们如何通过变长模板来是的模板能够接收任意多参数呢?答案是递归。

template  class tuple;     // 变长模板的声明
template        // 递归的偏特化定义
class tuple : private tuple {
Head head;
};
template<> class tuple<> {};                             // 边界条件
  • 首先需要声明一个变长模板(可能省略)
  • 然后偏特化这个变长模板(用于递归)
  • 最后定义一个变长模板的边界条件(一种特化版本)

可以看到对于tuple实例而言,会调用两参版本,将double传给Head,并将剩余的int,char,float参数,传给Tail;进一步的以tail进行包扩展递归调用,从而逐层获取参数,直到调用无参版本的tuple结束。

此外,对于无类型版本的接收任意多参数的模板如下:

#include 
using namespace std;
template  struct Multiply;
template 
struct Multiply {
static const long val = first * Multiply::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
    return 0;
}

上述代码实现的是参数的乘积。而且,由于val声明的是static const,因此所有的计算都发生于编辑期(即运行期无函数调用),此种方式称为模板元编程。

2.函数参数包

当可变参数用于函数模板时,称为函数参数包,语法如下:

template void f(T ... args);

值得注意的是,在C++11中,标准要求函数参数包必须唯一,且是函数的最后一个参数(模板参数包没有这样的要求)。

值得注意的是,在C++11中,标准要求函数参数包必须唯一,且是函数的最后一个参数(模板参数包没有这样的要求)

#include 
#include 
using namespace std;
void Printf(const char* s) {
    while (*s) {
        if (*s == '%' && *++s != '%')
            throw runtime_error("invalid format string: missing arguments");
        cout << *s++;
    }
}
template
void Printf(const char* s, T value, Args... args) {
    while (*s) {
        if (*s == '%' && *++s != '%') {
            cout << value;
            return Printf(++s, args...);
        }
        cout << *s++;
    }
    throw runtime_error("extra arguments provided to Printf");
}
int main() {
    Printf("hello %s\n", string("world"));   // hello world
}
// 编译选项:g++ -std=c++11 6-2-4.cpp

三、变长模板:进阶

前面提到了解包的表达式称为包扩展,实际上包能够展开的地方是有限制的,标准定义了七种位置:

❑ 表达式

❑ 初始化列表(列表初始化参见第3章)

❑ 基类描述列表

❑ 类成员初始化列表

❑ 模板参数列表

❑ 通用属性列表(第8章中会讲到)

❑ lambda函数的捕捉列表(第7章中会讲到)

除此之外的其他地方则不允许解包。

1.解包的细节

而对于解包有几个有几个有趣的点。

1.1.&&

对于声明了的变长模板参数包Args,如果我们使用Args&&...的方式解包,意味着展开后的参数为Arg1&&,Arg2&&,...,Argn&&。

1.2.Args...与(Args)...

对于表达式

template class T: private B...{};

与表达式

template class T: private B{};

所表达的含义是不一样的,前者表示多重继承B,B,B,而后者则表示B的多个模板参数B,类似的情况也会发生在函数身上:

#include 
using namespace std;
template void DummyWrapper(T... t) {}
template  T pr(T t) {
    cout << t;
    return t;
}
template
void VTPrint(A... a) {
    DummyWrapper(pr(a)...); // 包扩展解包为pr(1), pr(", ")..., pr(", abc\n")
};
int main() {
    VTPrint(1, ", ", 1.2, ", abc\n");
}

输出为

1, 1.2, abc

其中第10行采用pr(a)...的方式,表示将a展开后,逐个作为参数调用pr。而如果采用pr(a...)则表示pr接收模板参数包(当然这里并不支持),两者结果并不一样。

2.sizeof...

C++11引入变长模板的同时,扩展了sizeof,引入了sizeof...,用于计算参数包的参数个数,需要注意的是传入的参数为包名,而非形参名。

#include 
#include 
using namespace std;
template void Print(A...arg) {
    assert(false);  // 非6参数偏特化版本都会默认assert(false)
}
// 特化6参数的版本
void Print(int a1, int a2, int a3, int a4, int a5, int a6) {
    cout << a1 << ", " << a2 << ", " << a3 << ", "
        << a4 << ", " << a5 << ", " << a6 << endl;
}
template int Vaargs(A...args){
    int size = sizeof...(A);     // 计算变长包的长度
    switch(size){
        case 0: Print(99, 99, 99, 99, 99, 99);
            break;
        case 1: Print(99, 99, args..., 99, 99, 99);
            break;
        case 2: Print(99, 99, args..., 99, 99);
            break;
        case 3: Print(args..., 99, 99, 99);
            break;
        case 4: Print(99, args..., 99);
            break;
        case 5: Print(99, args...);
            break;
        case 6: Print(args...);
            break;
        default:
            Print(0, 0, 0, 0, 0, 0);
    }
    return size;
}
int main(void){
    Vaargs();    // 99, 99, 99, 99, 99, 99
    Vaargs(1);   // 99, 99, 1, 99, 99, 99
    Vaargs(1, 2);    // 99, 99, 1, 2, 99, 99
    Vaargs(1, 2, 3);     // 1, 2, 3, 99, 99, 99
    Vaargs(1, 2, 3, 4); // 99, 1, 2, 3, 4, 99
    Vaargs(1, 2, 3, 4, 5);   // 99, 1, 2, 3, 4, 5
    Vaargs(1, 2, 3, 4, 5, 6);    // 1, 2, 3, 4, 5, 6
    Vaargs(1, 2, 3, 4, 5, 6, 7);     // 0, 0, 0, 0, 0, 0
    return 0;
}

3.模板类型的模板参数包

实际上模板雷响的模板参数包和前面提到的类型模板参数包相似:

template class... B> struct Container{};
template class A, template class... B>
struct Container {
    A a;
    Container b;
};
template struct Container{};

这玩意可以将后面的template class...简化成一个类型,就好理解一点了,基本上跟前面的类型的模板参数包相似。

4.模板参数包与完美转发

#include 
using namespace std;
struct A {
A(){}
A(const A& a) { cout << "Copy Constructed " << __func__ << endl; }
A(A&& a) { cout << "Move Constructed " << __func__ << endl; }
};
struct B {
B(){}
B(const B& b) { cout << "Copy Constructed " << __func__ << endl; }
B(B&& b) { cout << "Move Constructed " << __func__ << endl; }
};
// 变长模板的定义
template  struct MultiTypes;
template 
struct MultiTypes : public MultiTypes{
T1 t1;
MultiTypes(T1 a, T... b):
t1(a), MultiTypes(b...) {
cout << "MultiTypes(T1 a, T... b)" << endl;
}
};
template <> struct MultiTypes<> {
MultiTypes<>(){ cout << "MultiTypes<>()" << endl;}
};
// 完美转发的变长模板
template