一、前言
我们在之前Linux的学习中了解过命令行参数,可以让我们在命令行中传入多个参数,并且之前在学习printf,scanf等接口时,接触过可变模版参数:
而今天学习的可变参数模板和普通模板的语义是一样的,只是写法上稍有区别,声明可变参数模板时需要在typename或class后面带上省略号“…”。比如我们常常这样声明一个可变模版参数:template<typename…>或者template<class…>。
二、可变模版参数概念
下边就是可变模板参数的基本形式:
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
- 上面的
参数args前面有省略号
,所以它就是一个可变模版参数
,我们把带省略号的参数称为“参数 包
”,它里面包含了0到N(N>=0)个模版参数。 - 使用sizeof…()可以获取模板参数包的参数个数和函数参数包的参数个数。
- 我们
无法直接获取
参数包args中的每个参数
的,只能通过展开参数包
的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。
使用sizeof…()可以获得参数包参数个数:
template<class ...Args>
void ShowList(Args... args)
{
cout << sizeof...(args) << endl;
}
int main()
{
ShowList(1);
ShowList(1, 2);
ShowList(11, 22, 'a');
ShowList(11, 22, 'a', "tmt");
ShowList();
return 0;
}
但是想要获取参数包中每个参数类型具体是什么需要采用下边的方法。
三、递归函数方式展开参数包
类似于递归函数,在设置模版参数时,首先设置一个具体参数T,再使用可变模版参数,这样我们就可以拿到第一个参数是什么类型,再次调用时将具体类型的后边的参数传入,这样就可以递归拿到参数包的参数类型,但是要切记必须另外有一个递归终止函数。
// 递归终止函数
template <class T>
void ShowList(const T& t)
{
cout << t << endl;
}
// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args)
{
cout << value <<" ";
ShowList(args...);
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
所以在递归函数体内部,就可以拿到参数包的参数,我们将其逐一打印出来。
四、逗号表达式展开
template <class T>
void PrintArg(T t)
{
cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... };//列表初始化+逗号表达式
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, 1.1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
- 这种展开参数包的方式,
不需要
通过递归终止函数
,是直接在ShowList函数体
中展开的,PrintArg
不是一个递归终止函数,只是一个处理参数包中每一个参数
的函数。 - 这种就地展开参数包的方式实现的关键是
逗号表达式
。我们知道逗号表达式会按顺序执行
逗号前面的表达式,并且最终执行结果
是最右边
的值。 - ShowList函数中的逗号表达式:(printarg(args), 0),按照这个执行顺序先执行printarg(args),再得到逗号表达式的结果0。
- 同时还用到了C++11的另外一个特性——
初始化列表
,通过初始化列表来初始化一个变长数组
,{(printarg(args), 0)...}
将会展开((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc... )
,最终会创建一个元素值都为0的数组int arr[sizeof...(Args)]
。 - 由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分PrintArg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包.
也就是类似于一个整形数组,使用初始化列表展开,但是由于可能传入的参数类型不同,所以我们使用逗号表达式,因为只要将最右边的参数类型都设为同一类型,便可以在第一个参数传入不同参数,只要在定义一个可以打印出第一个参数的函数,就可以获得所有参数。
五、STL容器中的empalce相关接口函数
C++11在vector容器中增加了emplace和emplace_back两个接口,他们就使用了可变模板参数:
emplace
emplace_back
vector容器之前的插入和尾插使用的是insert和push_back接口,那么C++11为什么要引入这两个接口呢?
我们首先来看下边的一种场景:
int main()
{
std::list< std::pair<int, char> > mylist;
// emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
// 那么在这里我们可以看到除了用法上,和push_back没什么太大的区别
mylist.emplace_back(10, 'a');
mylist.emplace_back(20, 'b');
mylist.emplace_back(make_pair(30, 'c'));
mylist.push_back(make_pair(40, 'd'));
mylist.push_back({ 50, 'e' });
for (auto e : mylist)
cout << e.first << ":" << e.second << endl;
return 0;
}
- 首先我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用。
- 在上边的一种场景中,push_back一个pair对象时,必须使用pair构造传参,或者make_pair来传参,但是emplace_back传参时,可以使用make_pair传参,也可以直接传入参数包。
- 对于这两种形式,push_back必须进行构造加拷贝构造或者移动构造,而emplace_back只需要一个构造,提高了效率。