【C++11】可变参数模板
一、可变参数模板概念以及定义方式
在C++11之前,类模板和函数模板只能含有固定数量的模板参数,c++11增加了可变模板参数特性:允许模板定义中包含0到任意个模板参数。声明可变参数模板时,需要在typename或class后面加上省略号“…”。
//Args是一个模板参数包,args是一个函数形参参数包
//声明一个参数包Args... args,这个参数包中可以包含0到任意个模板参数。
//模板参数包Args和函数参数包args的名字可以任意指定,并不是说必须叫做Args和args
template<class... Args>
void ShowList(Args... args)
{}
现在调用ShowList函数时就可以传入任意多个参数了,并且这些参数可以是不同类型的。比如:
template<class ...Args>
void ShowList(Args... args)
{}
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', string("sort"));return 0;
}
2.逗号表达式展开参数包
我们知道逗号表达式会按顺序执行逗号前面的表达式。
(PrintArg(args),0),也是按照这个执行顺序,先执行PrintArg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性 —— 初始化列表,通过初始化列表来初始化一个变长数组,{(PrintArg(args),0) … }将会展开成((PrintArg(arg1),0),(PrintArg(arg2),0),(PrintArg(args),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', string("sort"));return 0;
}
三、STL容器中的emplace相关接口函数
C++11标准给STL中的容器增加emplace版本的插入接口,比如vector容器的push_back和insert函数,都增加了对应的emplace_back和emplace函数。如下:
我们来看一下他们的声明
push_back在C++之后除了原来的左值版本外,还提供了右值版本,如果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表达式调用构造函数对空间进行初始化时,匹配到的是构造函数。
总结一下:
- 传入左值对象,需要调用构造函数+拷贝构造函数
- 传入右值对象,需要调用拷贝构造+移动构造函数
- 传入参数包,只需要调用构造函数