C++11中的变长模板
阅读本篇文章,必须熟悉掌握C++98中的模板。
C++中的变长模板真的是又臭又长,晦涩难懂,但是确实有些STL库就是这么写的。
1.C99中的变长函数
宏函数可以实现变长:就是采用C99中的变长宏__VA_ARGS__
,如下所示为C99代码:
#include<stdio.h>
#define LOG(...){\
fprintf(stderr,"%s: Line %d:\t",__FILE__,__LINE__);\
fprintf(stderr,__VA_ARGS__);\
fprintf(stderr,"\n");\
}
int main()
{
int x=3;
LOG("x=%d",x);//D:\study\c++\c++11\__VA_ARGS__.cpp: Line 10: x=3
}
普通函数也可以实现变长
#include<stdio.h>
#include<stdarg.h>
double SumOfFloat(int count,...)
{
va_list ap;
double sum=0;
va_start(ap,count);//使得ap初始化为参数列表的handle
for(int i=0;i<count;i++)
sum+=va_arg(ap,double);//每次读取sizeof(double)的字节
va_end(ap);
return sum;
}
int main()
{
printf("%f\n",SumOfFloat(3,1.2f,3.4,5.6));//10.200000
}
注意看上面代码中,
SumOfFloat
中的第一个参数count
,这就意味著,你必须告诉函数它的参数的具体个数,这就不太方便,例如SumOfFloat(3,1.2f,3.4,5.6)
中的第一个参数3
。
2.C++11中的变长函数
C++11实现上面功能的一种方法是:使用initializer_list
作为函数形参
#include<iostream>
#include<initializer_list>
using namespace std;
double SumOfFloat(const initializer_list<double>& l)
{
double sum=0;
for(auto &val:l)
sum+=val;
return sum;
}
int main()
{
printf("%f\n",SumOfFloat({1.2f,3.4,5.6}));//10.200000
}
这种思路感觉有点投机取巧,不太好
另一种方法是使用变长模板
#include<iostream>
template<typename... T>double SumOfFloat(T...);//模板函数声明
template<typename ...Args>//递归定义
double SumOfFloat(double a,Args... args)
{
return a+SumOfFloat(args...);
}
double SumOfFloat(double a){return a;}//边界条件
int main()
{
printf("%f\n",SumOfFloat(1.2f,3.4,5.6));//10.200000
}
先不对上述代码做解释,只要知道<typename... T>
中的T
叫做模板参数包,它是类型的,是一种特殊的类型,它可以被推导为多个类型的pack。即SumOfFloat(1.2f,3.4,5.6)
会将T
推导为double,double,double
的一种pack类型。
除此之外,必须知道这种设计必须是递归定义的。
3.详解变长模板
首先讲一个前置概念:SFINAE
.
3.1 更一般的SFINAE
规则
在C++98中,我们就有SFINAE
法则:Substitution failure is not an error
即匹配失败不算失败,在C++中就是,即使模板展开失败,也不会报错
struct Test
{
typedef int FOO;
};
template<typename T>
void foo(typename T::FOO){}
template<typename T>
void foo(T){}
int main()
{
foo<Test>(10);
foo<int>(10);
}
上面代码在C++98中也可以通过编译,上面
foo(typename T::FOO)
中typename
显式的表示,T::FOO
是一个类型,在foo<int>(10);
中,编译器会尝试用 第一个模板函数来匹配它,但是会发现int::FOO
错误,但是编译器不会报错,这就是SFINAE
在C++11中对SFINAE
规则做了放松,
template<int I>
struct A {};
char xxx(int);
char xxx(float);
template<typename T>A<sizeof(xxx((T)0))> f(T){}
int main()
{
f(1);
}
有一些C++98编译器会对上式报错,这是因为它们认为模板参数过于复杂,即这里的
sizeof(xxx((T)0))
,不过现在的编译器都狠优秀,它们可以完成这种表达式的推导,C++11的标准是:只要表达式中没有出现外部于表达式本身的元素,编译器都可以完成推导
3.2 模板参数包的概念
接下来的内容会有一定难度。
我们知道,在C++98中模板参数有3种:类型的,非类型的,模板类型的。
template<typename T,int i,template<typename> class A>
中T
就是类型的,i
是非类型的,A
是模板类型的。
在C++11中,我们为了支持变长的模板,我们加入一种新的模板参数:模板参数包。
所以在C++11中模板参数有4种:类型的,非类型的,模板类型的和模板参数包。
而模板参数包又可以细分为3种:类型的模板参数包,非类型的模板参数包,模板类型的模板参数包。
模板参数包是一种pack,下面我们从模板推导角度来解释这种pack:
- (类型的)模板参数包
template <typename T1,typename T2>class B{};
template <typename... A>class Template: private B<A...>{};
Template<X,Y> xy;
上面中,<typename...A>
中A
是 (类型的)模板参数包,它可以接收任意多个类型参数作为模板参数,具体来说,Template<X,Y>
会将A
推导为X
和Y
类型的pack。
B<A...>
中A...
是一种包扩展,它是模板参数包unpack的结果。由于A
被推导为X
和Y
的pack,所以A...
就被具体解释为X,Y
,然后具体化为B<X,Y>
。
如果我们使用Template<X,Y,Z> xyz
就会引发推导错误,没有任何一个模板适配,这是因为此时A...
被解释为3个类型:X,Y,Z
,它无法和B
匹配。
- (非类型的)模板参数包
template<int i,long j,unsigned int k>class B{};
template<int ...A> struct Pack: private B<A...>{};
Pack<1,0,2> data;
<int ... A>
中A
是 (非类型的)模板参数包,它可以接收分离多个非类型参数作为模板参数,具体来说,Pack<1,0,2>
会将A
推导为整值1
,0
,2
的pack,而B<A...>
中A...
是一种包扩展,由于A
推导为整值1
,0
,2
的pack,所以A...
被具体解释为1,0,2
,然后具体化为B<1,0,2>
- (模板类型的)模板参数包
template <typename T> class A;
template <typename T> class B;
template<template<typename> class T1,template<typename> class T2> class C{};
template<template<typename> class ...T> struct Pack:private C<T...>{};
Pack<A,B> data;
<template<typename> class ...T>
中T
是 (模板类型的)模板参数包,它可以接收多个模板作为模板参数,具体来说,Pack<A,B>
会将T
推导为A
和B
的pack,而C<T...>
中T...
就是一种包扩展,由于T
推导为A
和B
的pack,所以T...
就被具体解释为A,B
,然后具体化为C<A,B>
3.3 三个简单的例子
变长模板必须采用递归设计,下面是3个简单的例子,请仔细阅读。
- (类型的)模板参数包的使用
下面给出C++11中tuple
的简单实现,
template<typename... Elements> class tuple;//模板声明
template <typename Head,typename... Tail>//递归定义
class tuple<Head,Tail...>:private tuple<Tail...>
{
Head head;
};
template<> class tuple<>{};//边界条件
同样也是递归定义,这种递归的设计就是变长模板最晦涩的地方。
当实例化tuple<double,int,char,float>
类时,
第一次:Head
被推导为double
,Tail...
被推导为int,char,float
第二次:Head
被推导为int
,Tail...
被推导为char,float
第三次:Head
被推导为char
,Tail...
被推导为float
第三次:Head
被推导为float
,Tail...
被推导为空
最后由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;
cout<<Multiply<22,44,66,88,9>::val<<endl;
}
上面这种编程方式,叫做模板元编程,他将乘法的计算放到模板推导过程中,就是把计算过程放在编译阶段,这样运行时就不需要计算了
- (模板类型的)模板参数包的使用
template <typename T> class Module1{};
template <typename T> class Module2{};
template<typename I,template<typename>class ... B>struct Container;//模板声明
template<typename I,template<typename> class A,template<typename> class... B>
struct Container<I,A,B...>//递归定义
{
A<I> a;
Container<I,B...> b;
};
template<typename I> struct Container<I>{};//边界条件
int main()
{
Container<int,Module1,Module2> a;
}
3.4 函数参数包
函数参数包是在写变长模板函数中的一个概念
函数参数包也是一种pack型变量,它也存在unpack,包扩展的概念。
void g(int,char,double);
template<typename ... T>
void f(T... args)
{
g(args...);
}
f(1,'c',1.2);
在<typename ... T>
中的T
是 (类型的)模板参数包 。
在f(T... args)
中T...
叫做 包扩展
在f(T... args)
中的args
是一种类型为T...
的变量,它叫函数参数包
在g(args...)
中的args...
也是一种包扩展,它是将args
unpack后的产物
例如,这里f(1,'c',1.2)
就会将T
推导为int,char,double
的pack,于是T...
就被具体解释为int,char,double
,然后args
就是类型为T...
的一种变量,args
的值是1,'c',1.2
的pack,则我们可以在f
中调用g(args...)
完成对args
的unpack。
下面看一个,C++11中提案的prinf()
函数的实现
#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!='%')//确保`%%`不出现
{
cout<<value;
return Printf(++s,args...);
}
cout<<*s++;
}
throw runtime_error("extra arguments provided to Printf");
}
int main()
{
Printf("hello %s\n",(string)"world");
}
变长模板的难点在于我们不知道参数的个数,我们必须采用递归定义,就像上面的Printf
就是递归定义的,采用的是数学归纳法,可以细细品味一下上面那段代码。
3.5 包扩展的进阶
...
符号可以放在意想不到的地方,例如:
template<typename... A> class T:private B<A>...{};//#1
template<typename... A> class T:private B<A...>{};//#2
对于实例化T<X,Y>
,#1
会被解释为
class T<X,Y> class T:private B<X>,private B<Y>{};
#2
会被解释为
class T<X,Y> class T:private B<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)
{
DummyWrapper(pr(a)...);
}
int main()
{
VTPrint(1,", ",1.2,", abc\n");
}
上面这段代码,某些编译器(例如g++)的结果是逆序的:
, abc
1.2, 1
应该是,不同的编译器可能包扩展的顺序不太一样,有些的逆序的。
下面我们看一段晦涩难懂的代码
#include<iostream>
#include<tuple>
using namespace std;
template<typename A,typename B>
struct S
{
int a=1;
};
template<
template<typename...> class T, typename... TArgs,
template<typename...> class U, typename... UArgs
>
struct S< T<TArgs...> , U<UArgs...> >{int a=2;};
int main()
{
S<int,float> p;
S<tuple<int,char>,tuple<float>> s;
//S<tuple,int,char,tuple,float> s;编译出错
cout<<s.a<<endl;//2
}
注意上面这段代码中,最终输出是2,奇怪的地方在于,S<tuple<int,char>,tuple<float>>
如何匹配第二个模板呢?这种设计是约定俗称的,没有任何原因,记住上面这种巧妙的设计就行了。
3.6 sizeof...()
的使用
sizeof...()
其实狠简单的,它就是获得pack
中的变量的个数,有些用的
#include<cassert>
#include<iostream>
using namespace std;
template<typename... A>
void Print(A... arg)
{
assert(false);
}
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...(args);//或者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);
}
}
int main()
{
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
}
3.7 变长模板和完美转发的配合
#include<iostream>
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<typename... T> struct MultiTypes;//模板声明
template<typename T1,typename... T>//递归定义
struct MultiTypes<T1,T...>: public MultiTypes<T...>
{
T1 t1;
MultiTypes<T1,T...>(T1 a,T... b):t1(a),MultiTypes<T...>(b...)
{
cout<<"MultiTypes<T1,T...>(T1 a,T... b)"<<endl;
}
};
template<> struct MultiTypes<>//边界条件
{
MultiTypes<>(){cout<<"MultiTypes<>()"<<endl;}
};
template<template<typename...> class VariadicType,typename... Args>
VariadicType<Args...> Build(Args&& ... args)
{
return VariadicType<Args...>(std::forward<Args>(args)...);
}
int main()
{
A a;
B b;
Build<MultiTypes>(a,b);
//等价于Build<MultiTypes,A,B>(a,b);
}
MultiTypes<>()
MultiTypes<T1,T...>(T1 a,T... b)
MultiTypes<T1,T...>(T1 a,T... b)
没啥好说的,这就是完美转发,它根部就不调用移动构造和拷贝构造函数,完全都是靠引用传递的