Variaidc Templates(数量不定的模板参数)
释义
这个特性可以使得函数接收数量不定的参数进行处理。
先看示例,比如我们常见的print函数,我们把它写成函数模板
template<typename T,typename...Types>
void print(const T& firstArg,const Types&... args){
cout<<firstArg<<endl;
print(args...);
}
这里相当于把参数分为T类型的firstArg和剩下的Types类型的args。
首先要注意的是template<>的写法,…出现在typename之后
再来看函数内参数的写法,…出现在类型Types的后面。
根据调用我们来理解上述的过程
print(7.5,"hello",bitset<16>(377),42);
这里传入了四个参数。
print会把他们分为第一个和剩下的其他所有。然后输出7.5
当再次调用print(args…)时,又会把剩余的参数分解,再输出hello。
整个过程是一个递归的函数调用。
泛化与特化
如果还有一个print
template<typename...Types>
void print(const Types&... args){
...
}
与上面的print可以并存么?会不会出现歧义?答案是可以的。
来看这样一个例子:
class CustomerHash{
public:
std::size_t operator()(const Customer& c)const{
return hash_val(c.fname,c.iname,c.no);
}
};
下面则是hash_val的三个版本。
版本1:
template<typename T,typename...Types>
inline void hash_val(size_t& seed,const T& val,const Types&... args){
hash_combine(seed,val);
hash_val(seed,args...)
}
版本2:
template<typename...Types>
inline size_t hash_val(const Types&... args){
size_t seed=0;
hash_val(seed,args...);
return seed;
}
版本3:
template<typename T>
inline void hash_val(size_t&& seed,const T& val){
hash_combine(seed,val);
}
我们根据调用中的return hash_val(c.fname,c.iname,c.no);来解释究竟调用了哪个版本。
-
第一次调用时,由于c.fname不是size_t类型的,所以调用版本2来接受所有参数,这是定义size_t seed =0
-
第二次调用时为hash_val(seed,args…),这时候版本1和版本2其实都是符合的,但版本1算作特化,也就是更加符合(有seed),所以调用版本1,把原来的一包参数分解为val和剩下的args
-
多次调用之后,当参数里只剩下2个参数时,也就是seed和val,这时候会调用版本3收尾。
-
可以看出整个过程是对于不定量参数的不断拆分,通过递归调用来实现的。
-
nullptr
c++11之后允许使用nullptr代替0或者NULL。之前使用NULL代表指针为空,但其实NULL本身还是0。
而现在的nullptr是指针类型的空,更好地表示指针为空。
auto关键字
auto关键字可以由编译器自动推导出该变量的类型。
double f();
auto d=f();//d是double类型
但不建议用于所有类型推导,因为知道具体的类型更有益于理解程序。
一般用于类型名比较长或者比较复杂的情况。
list<string> c;
list<string>::iterator ite;
ite=c.begin();
auto ite=c.begin();
可以省去较长的类型名。
一致性初始化
在C++11之前,初始化可以使用大括号,中括号或者等于号来操作。
而现在可以统一使用大括号,也就是在变量后面加{}进行初始化。
int value[]{1,2,3};
vector<int>v{2,3,5,7,11,13,17};
complex<double>c{4.0,3.0};
这里要谈谈背后的实现。
编译器在看到这个{}时,会做出一个initializer_list(初始化列表),会将其关联至array<T,n>,这里的T就是数据的类型,n就是个数。
然后调用构造函数时,这一堆数据可以被编译器分解为单个数据传给构造函数。比如上面的complex,编译器把4.0和3.0单独传给complex的构造函数来实现初始化。
但也有一些区别:比如vector的构造函数有多个版本,其中的一个版本可以直接接受整个initializer_list,不需要分解。
而complex就没有直接接受initializer_list的构造函数。
initializer_list
作用
上面在初始化提到的initializer_list在很多地方还有大用处。
这个列表主要实现了可以传入任意数量的某类型的参数的功能。
来看一个例子
class P{
public:
P(int a,int b)
{
cout<<"P(int,int),a="<<a<<",b="<<b<<endl;
}
P(initializer_list<int> initlist)
{
cout<<"P(initializer_list<int>),value=";
for(i:initlist)
cout<<i<<'';
cout<<endl;
}
};
P有两个版本的构造函数,第一个版本是我们熟悉的,第二个版本是initializer_list的,可以看出第二个版本可以将传入的参数进行循环处理。
当我们调用时
P p(77,5);//1
P p{77,5};//2
P r{77,5,42};//3
P s={77,5};//4
1会直接调用第一个版本的构造函数(也是上面complex的情况)
而2,3,4因为{}的存在,都会调用initializer_list的版本。
回到上面的疑问,如果我们没有initializer_list版本的构造函数会发生什么呢?
编译器会把{}中的参数逐一传给普通版本的构造函数,所以1,2,4还是可以实现的,但3就会出现错误。
内部实现
class initializer_list{
...
private:
iterator _M_array;
size_type _M_len;
...
}
initializer_list的内部其实是由array和其长度组成的,和我们前面说的关联至array<T,n>一致。
需要注意这里的array其实是一个迭代器,指向这个数组空间,并不是数组本身。
array是标准库中的容器,这样设计便于调用接口。
所以在拷贝initializer_list时需要明白其实只是拷贝了迭代器,属于浅拷贝(link),是很危险的操作。
新的用法
initializer_list在标准库中被大量的使用。主要用于接受不定数量的参数。
这也意味着我们在使用一些容器或者算法时可以更加灵活。
vector<int>v1{2,3,4,5,6,7};
vector<int>v2({2,3,4,5,6,7});
vector<int>v3;
v3={2,3,4,5,6.7};
max({string("ace"),string("bds")});
min({43,32,451,3254});
类似这样对于initializer_list{}的使用都可以让程序变得更加简洁。