变长模板

变长模板

变长函数和变长模板参数

我们知道C++11已经支持了C99的变长宏,但是,无论是宏,还是变长参数(C语言中存在),整个机制的设计上, 没有任何一个对于传递参数的类型是了解的。我们可以看看变长函数的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
double SumOfFloat(int count,...){
va_list ap;
double sum=0;
va_start(ap,count); //获得变长列表的句柄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
}
//参考下面链接,熟悉下C语言中变长函数的写法及原理

只有使用表达式va_arg(ap,double)的时候,我们 才按照类型(实际是按类型长度)去变长参数列表中获得指定参数,以及为ap找到下一个参数的位置。对于printf函数来说,如何打印则得益于传递在字符串中的形如“%s”、“%d”这样的转义字,这都是对连续内存的一种解释,因此,对于一些没有定义转义字的非POD的数据来说,使用变长函数就会导致未定义的程序行为

1
2
3
4
const char* msg = "hello%s";
printf("msg",std::string(" world"));
//程序将报错,
//无法通过可变参数函数传递非平凡类型“std::string”(又名“basic_string<char>”)的对象; 调用将在运行时中止

对于c++这种强调类型的语言来说,是不愿意看到的,即使他是正确的。c++需要一种更现代的传参方式即类型和变量同时能够传递给变长参数的函数。一个好的方式就是使用C++的函数模板,在C++98 中,标准要求函数模板始终具有数目确定的模板参数及函数参数。

c++11中标准模板库存在 tuple类,在C++11中,tuple是pair类的一种更为泛化的表现形式。比起 pair,tuple是可以接受任意多个不同类型的元素的集合。

1
std::tuple<double,char,std::string> cloolctions

因为tuple可以接受任意多的参数。此外,和pair类似地,我们也可以更为简单地使用C++11的模板函数make_tuple来创造一个tuple 模板类型。

1
std::make_tuple(9.8,'g',"gravity");

在C++11中我们看到了所谓的变长模板(variadic template)的实现。

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

变长模板的的语法,以tuple为例:

1
template<typename ...Elements> class tuple;

在c++11中,我们使用 … 来表示参数的变长的,Elements被称为模板参数包,有了模板参数包,类模板tuple就可以接受多个参数做为模板参数,如:tuple<int , char , double> ,编译器则可以将多个模板参数打包成为“单个的”模板参数包 Elements,即Elements在进行模板推导的时候,就是一个包含int、char 和double三种类型类型集合。

与普通模板相似,模板参数包也可以是非类型的

1
2
3
4
5
6
7
//如:
templateint...A>class NonTypeVariadicTemplate{};
NonTypeVariadicTemplate<1,0,2>ntvt;
//相当于:
templateint,int,intclass NonTypeVariadicTemplate{};
NonTypeVariadicTemplate<1,0,2>ntvt;
//除了类型的模板参数包和非类型的模板参数包,模板参数包实际上还是模板类型的,之后讨论

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

1
tempalte <typename ...A> class Template:private B <A...>{};

这里的...A是表示课接受多个模板参数,参数包名为 A , A...是一个包拓展,参数包会在包扩展的位置展开为多个参数

1
2
3
template<typename T1,typename T2> class B{};
template<typename ...A> class Template:private B<A...>{};
Template<X,Y> xy;

如何才能利用模板参数包及包扩展,使得模板能够接受任意多的模板参数,且均能实例化出有效的对象呢?

在C++11中,实现tuple模板的方式给出了一种使用模板参数包的答案。这个思路是使用数学的归纳法,转换为计算机能够实现的手段则是递归。通过定义递归的模板偏特化定义,我们可以使得模板参数包在实例化时能够层层展开,直到参数包中的参数逐渐耗尽或到达某个数量的边界为止。

1
2
3
4
5
6
7
8
9
10
11
12
template<typename...Elements>class tuple;   //变长模板的声明
template<typename Head,typename...Tail> //递归的偏特化定义
class tuple<Head,Tail...>:private tuple<Tail...>{
Head head;
};
template<>class tuple<>{}; //边界条件
int main()
{
tuple<int, double,float> t;
//t.head = 20;
return 0;
}

我们声明了变长模板类tuple,其只包含一个模板参数,即Elements模板参数包。此外,我们又偏特化地定义了一个双参数的tuple的版本。该偏特化版本的tuple包含了两个参数,一个是类型模板参数Head,另一个则是模板参数包Tail,将Head型数据作为第一成员,而将使用了包扩展表达式的模板类tuple<Tail…>作为tuple<Head,Tail… >的私有基类。这样一来,当程序员实例化一个形如tuple< double,int,char,float>的类型时,则会引起基类的递归构造,这样的递归在tuple的参数包为0个的时候会结束。

使用非类型模板的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
return 0;
}

c++11中我们还可以声明变长的模板函数

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

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

有了模板参数包和函数参数包两个概念,我们就可以实现C中变长函数的功能了。我们可以看看这个C++11提案中实现新的printf的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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!='%'){
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
}

相比于变长函数(printf),变长函数模板(Printf)不会丢弃参数的类型信息。因此重载的cout的操作符 << 总是可以将具有类型的变量正确地打印出来。

变长模板进阶

c++11中可以展开参数包的位置:

表达式 初始化列表

基类描述列表 类成员初始化列表

模板参数列表 通用属性列表

lambda函数的捕获列表

一些有趣的包拓展语法:

1
2
3
4
5
6
template <typename ...Arg> void func(Arg&&...){}
//解包为:Arg1&& , ... , Argn&&

template <typename ...A> class T:private B<A>...{}
//当使用 T<X,Y> 实例化时,会解包为
class T<X,Y>:private B<X> , private B<Y> {} //会解包为多重继承

类似的现象也会发生在函数模板上:

1
2
3
4
5
6
7
8
9
10
11
12
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){
DummyWrapper(pr(a)...);//包扩展解包为pr(1),pr(",")...,pr(",abc\n")
};
int main(){
VTPrint(1,",",1.2,",abc\n");
}

在C++11中,标 准还引入了新操作符sizeof...其作用是计算参数包中的参数个数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
template<class...A>
void Print(A...arg) {
assert(false);//非6参数偏特化版本都会默认assert(false)
}

//特化6参数的版本,若匹配失败则执行Print(A...arg)后执行assert
void Print(int a1, int a2, int a3, int a4, int a5, int a6) {
cout << a1 << "," << a2 << "," << a3 << ","
<< a4 << "," << a5 << "," << a6 << endl;
}

template<class...A>
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;
}

后面看不太懂了

参考:书籍《深入理解c++11》
C语言变长参数函数原理_code_peak的博客-CSDN博客