这个应该算是C++11, 即2.0时代的新特性吧, 比较有意思.(tuple中也有使用)
简介
最早在printf的时候接触过变长参数(参数个数不确定), C++11也采用了包扩展来支持模板支持任意参数. 所谓的包扩展, 就是指一组参数, 数量不确定(取决于你调用传入的时候传入的参数), 类型也是.
我一般把它简单理解成数组, 方便.
具体的例子:1
2
3
4template<typename T, typename... Types>
void print(const T& firstArg, const Types&...args){
//省略实现
}
可变参数, 一般放在参数列表末尾, 否则编译器报错.
二义性问题
重载, 会不会存在二义性问题呢?
例如:1
2
3
4
5
6template<typename T, typename... Types>
void print(const T& firstArg, const Types&...args){}
//对比
template<typename T, typename... Types>
void print(const Types&...args){}
对于编译器而言, 编译器总是找最佳匹配, 所以他们是可以共存的, 而且优先调用特化的版本, 即第一个版本.
使用场景
函数递归调用
大家一般使用这样的变长模板参数, 主要是用于递归
的场景.
例如上面变参模板可以这样实现递归调用:1
2
3
4
5
6
7void print(){}
template<typename T, typename... Types>
void print(const T& firstArg, const Types&...args){
cout << firstArg << endl;
print(args...);
}
最后一次递归调用会调用到 void print()
这个版本(递归一定要设置终止条件的), 没有的话, 就编译报错了.
主要用途, 用于帮助递归, 分解不定个数的参数. (使用递归(recursive)处理包(pack)参数)
上面那个例子只是用于演示, 没有实际作用, 但是它也演示了, 在使用变参模板的重载时, 如果有最佳匹配, 那么总是以最佳匹配优先(严格匹配优先, 特化匹配优先), 没有上述情况的时候, 才会匹配泛化的函数模板.
递归继承
tuple
类的实现(你可以简单的把tuple看做一组对象组合在一起形成一个对象), 采用的就是递归继承.
1 | template<typename Head, typename...Tail> |
从上面, 你也可以看出整个tuple的构建过程, 每次继承, 只保留Head部分, 然后用this指针指向父类的部分, 即tail部分, 这就是递归的过程.
举个具体的例子:
1 | tuple<int, float, string> t(1, 2.0, "hello"); |
继承关系是怎么样的呢?
1 | tuple<> |
本类只保存第一个参数, 当做head, 然后实现这个类, 该类的父类被当做tail部分, 依次递归下去.
你可以当做组合关系, 一层层组装, 每一层只和相邻的层有关系(区分一下和继承图的关系):
试试看下面的代码:
1 | tuple<int, float, string> t(1, 2.0, "hello"); |
递归组合
如果你不喜欢用继承
, 可以把tuple
改成组合的方式, 如下:
1 | //先写递归结束条件, 因为是模板, 所以可以同时存在多个(参数不同) |
调用逻辑还是一样的,
1 | tup<int, float, string> tt(3, 2.0, "merlin"); |
单独使用
好像泛型参数模板使用的时候, 都要
指定具体的类型么
?
例如我下面有个泛型参数模板, 如下:
1 | //递归终止时的调用 |
使用的时候, 不指定泛型参数, 如cout << maximum(1,2,3,4,5) << endl;
可否?
1 |
|
完整代码自己可以试一下, ok的 (调用过程是, 5Vs4, 然后从右边不断向左推进比较).
使用可变参数模板时,
不必为了专门的去特化而指定<>里的内容
, 也没法指定;变参泛型模板自己会推导
上面的例子, 同类型的不定参数最好用initializer_list
.
注意事项
这个变参...
的写法, 不仅仅是函数参数一项, 所以每次到底是在参数前面, 还是在参数后面, 非常烦人.
例如上面的:
1 | template<typename Head, typename...Tail> |
当然, 当初设计它的人, 绝对不是神经病, 所以还是有规律的, 简单记忆如下:
- 跟在
typename
后面(即使类型出现), 其他情况只要类型出现, 一定跟在类型(引用)后面; - 使用的时候(比如函数调用)放在具体实参后面, 例如
args...
. - sizeof出现, 跟在sizeof的后面,
sizeof...(args)
可以拿到参数个数
其实总是跟在别人后面, 只是不停的在换老大, 有typename
的时候跟在typename
后面