目录
一、可变参数模板概念以及定义方式
二、参数包的展开
1. 递归函数方式展开参数包
2. 逗号表达式展开参数包
三、STL容器中的empalce相关接口函数
一、可变参数模板概念以及定义方式
在c++11之前,类模板和函数模板只能含有固定数量的模板参数,c++11增加了可变模板参数特性:允许模板定义中包含0到任意个模板参数。声明可变参数模板时,需要在typename或class后面加上省略号"..."。
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
// 模板参数包Args和函数形参参数包args的名字可以任意指定,并不是说必须叫做Args和args
template <class ...Args>
void ShowList(Args... args)
{}
现在调用ShowList函数时就可以传入任意多个参数了,并且这些参数可以是不同类型的。比如:
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', string("sort"));
return 0;
}
我们可以在函数模板中通过sizeof计算参数包中参数的个数。比如:
template<class ...Args>
void ShowList(Args... args)
{
cout << sizeof...(args) << endl; //获取参数包中参数的个数
}
二、参数包的展开
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。
1. 递归函数方式展开参数包
递归展开参数包的方式如下:
- 给函数模板增加一个模板参数,这样就可以从接收到的参数包中分离出一个参数出来。
- 在函数模板中递归调用该函数模板,调用时传入剩下的参数包。
- 如此递归下去,每次分离出参数包中的一个参数,直到参数包中的所有参数都被取出来来。
- 还需要给一个递归终止函数。
// 递归终止函数
// 当递归调用ShowList函数模板时,如果传入的参数包中参数的个数为0,那么就会匹配到这个无参的递归终止函数,这样就结束了递归。
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;
}
2. 逗号表达式展开参数包
我们知道逗号表达式会按顺序执行逗号前面的表达式。
(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数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。
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, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
三、STL容器中的empalce相关接口函数
C++11标准给STL中的容器增加emplace版本的插入接口,比如vector容器的push_back和insert函数,都增加了对应的emplace_back和emplace函数。如下:
我们来看一下他们的声明
push_back在C++11之后除了原因的左值版本外,还提供了右值版本,如果push_back的是左值那么就调用左值版本,反之就是右值引用的版本;但是只能支持单个元素的插入。
emplace_back和emplace本质的区别就是他们采用了可变参数模板,这样一来,他就可以支持多个元素的插入,代码如下:
int main()
{
/********************emplace_back***************************/
vector<int> v1;
vector<int> v2;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
//v1.push_back({1,2,3,4,5}); // 不可以
v2.emplace_back(1, 2, 3, 4, 5);
/**********************emplace*************************/
v1.insert(v1.begin(), 9);
v1.insert(v1.begin(), 8);
v1.insert(v1.begin(), 7);
v1.insert(v1.begin(), 6);
//v1.insert(v1.begin(), 9, 8, 7, 6); // 不可以
v2.emplace(v2.begin(), 9, 8, 7, 6);
return 0;
}
由于emplace系列接口的可变模板参数的类型都是万能引用,因此既可以接收左值对象,也可以接收右值对象,还可以接收参数包。
- 如果调用emplace系列接口时传入的是左值对象,那么首先需要先在此之前调用构造函数实例化出一个左值对象,最终在使用定位new表达式调用构造函数对空间进行初始化时,会匹配到拷贝构造函数。
- 如果调用emplace系列接口时传入的是右值对象,那么就需要在此之前调用构造函数实例化出一个右值对象,最终在使用定位new表达式调用构造函数对空间进行初始化时,就会匹配到移动构造函数。
- 如果调用emplace系列接口时传入的是参数包,那就可以直接调用函数进行插入,并且最终在使用定位new表达式调用构造函数对空间进行初始化时,匹配到的是构造函数。
总结一下:
- 传入左值对象,需要调用构造函数+拷贝构造函数。
- 传入右值对象,需要调用构造函数+移动构造函数。
- 传入参数包,只需要调用构造函数。