1. 简单介绍与基本语法
可变参数模板是指模板的类型参数列表的的参数个数可变。
C++11支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包,存在两种参数包:
模板参数包:表示零或多个模板参数。
函数参数包:表示零或多个函数参数。
参数包的加入使得我们可以更加方便地设计出参数个数可变的函数,就比如模拟实现printf。
但C语言由于不支持模板,所以实现printf的方式十分复杂,查了我也没看明白。
声明该类型模板的语法
template<class ...Args> Type Func(Args... args) {}template<class ...Args> Type Func(Args&... args) // 每个参数都按左值引用方式接收{}template<class ...Args> Type Func(Args&&... args) // 每个参数都按万能引用方式接收{}
省略号用来指出一个由模板参数或函数参数构成的一个包:
在模板参数列表中,class...或typename...指出接下来的参数表示零或多个类型列表。
在函数参数列表中,类型名后面跟...指出接下来表示零或多个形参对象列表。
函数参数包可以用左值引用或右值引用表示,跟前面普通模板一样,每个参数实例化时遵循引用折叠规则。
可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
"sizeof..."运算符
这里我们引入一个新的运算符 "sizeof..." ,用于计算参数包中参数的个数:
template <class ...Args>void Print(Args&&... args){ cout << sizeof...(args) << endl;}int main(){ double x = 2.2; Print(); // 包⾥有0个参数 Print(1); // 包⾥有1个参数 Print(1, string("xxxxx")); // 包⾥有2个参数 Print(1.1, string("xxxxx"), x); // 包⾥有3个参数 return 0;}
2. 包扩展(参数包展开)
参数包并不能如我们所想地去直接访问其内容,我们只能采取如下两种方式来访问其包含的参数:
递归展开
虽然我们不能直接取得参数包中的参数,但是在用参数包进行传参时,参数包中的参数会自动匹配形参。
由此为启发,我们可以设计一个参数列表为一个参数和一个参数包的递归模板函数:
Type Func(){ // ...}template<class T, ...Args>Type Func(T& x, Args... args){ // ... Func(args...);}
函数体中是对单个参数 "x" 进行操作的逻辑。
注意,不能将无参重载版本去掉而改成下面这样:
template<class T, ...Args>Type Func(T& x, Args... args){ // ... if(sizeof...(args) != 0) Func(args...);}
假如按照这样的方式来写,无参的Func虽然不会被调用,但是在编译阶段,编译器会尝试实例化出无参的版本。
而我们给出的模板至少接收一个参数,编译器无法实例化出无参Func,会直接报错。
通过这样的方式,我们可以将参数包中的参数从前向后一个一个地拆出来,分别操作。
例如:
void ShowList(){ // 编译器时递归的终⽌条件,参数包是0个时,直接匹配这个函数 cout << endl;} template <class T, class ...Args>void ShowList(T x, Args... args){ cout << x << " "; // args是N个参数的参数包 // 调⽤ShowList,参数包的第⼀个传给x,剩下N-1传给第⼆个参数包 ShowList(args...);} // 编译时递归推导解析参数template <class ...Args>void Print(Args... args){ ShowList(args...);} int main(){ Print(); Print(1); Print(1, string("xxxxx")); Print(1, string("xxxxx"), 2.2); return 0;}
也可一次拆出多个参数,在递归函数中增加几个形参即可。但此时就需要多写几个重载函数来处理参数个数不够多的情况了。
复合函数
template <class T>const T& GetArg(const T& x){ // ... // 返回值不一定是x,可根据需要确定 return x;} template <class ...Args>void Arguments(Args... args){}template <class ...Args>void test(Args... args){ // 注意GetArg必须返回或者到的对象,这样才能组成参数包给Arguments Arguments(GetArg(args)...);}
"Arguments(GetArg(args)...);" 的含义是,将参数包中的参数依次传入GetArg函数中进行调整之后,形成新的参数包传给Arguments函数。
这样的写法不仅可以用于展开参数包,还可以用于需要对参数包每个参数都进行处理之后进行函数调用的场景。
注意:Arguments函数的调用是必要的,括号内的 "GetArg(args)..." 并不能独立作为表达式而存在。
3. emplace系列接口
template <class... Args> void emplace_back (Args&&... args);template <class... Args> iterator emplace (const_iterator position, Args&&... args);
C++11以后STL容器新增了empalce系列的接口,empalce系列的接口均为模板可变参数,能够完成与push_back,insert等相同的功能,而参数包的存在使得emplace系列接口不止可以接受要插入的对象,还可以接受构造要插入对象的参数。
以前我们在进行插入时,要么用已有对象,要么用匿名对象,要么用隐式类型转换,总之在进入函数之前,对象就需要被构造好,进入函数之后再通过拷贝构造或移动构造,构造出形参进行插入。
而emplace系列接口的参数包可以直接接受构造对象的参数而不用发生隐式类型转换,在函数内部直接将参数包层层传递(如果复用了其他函数的话)给构造函数构造出对象,并进行插入。
template <class... Args>void emplace_back(Args&&... args){insert(end(), std::forward<Args>(args)...);}template <class... Args>iterator emplace(const_iterator pos, Args&&... args){Node* cur = pos._node;Node* newnode = new Node(std::forward<Args>(args)...);Node* prev = cur->_prev;// prev newnode curprev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;return iterator(newnode);}
也就是说emplace系列接口可以将函数外的构造转移到函数内进行构造,以此节省掉一次拷贝构造或移动构造。
emplace系列总体而言是更高效的,推荐以后使用emplace系列替代insert和push系列。
#include<list>int main(){list<string> lt;// 传左值,跟push_back一样,走拷贝构造string s1("111111111111");lt.emplace_back(s1);// 右值,跟push_back一样,走移动构造lt.emplace_back(move(s1));// 直接把构造string参数包往下传,直接用string参数包构造string// 这里达到的效果是push_back做不到的lt.emplace_back("111111111111");list<pair<string, int>> lt1;// 跟push_back一样// 构造pair + 拷贝/移动构造pair到list的节点中data上pair<string, int> kv("苹果", 1);lt1.emplace_back(kv);// 跟push_back一样lt1.emplace_back(move(kv));// 直接把构造pair参数包往下传,直接用pair参数包构造pair// 这⾥达到的效果是push_back做不到的lt1.emplace_back("苹果", 1);return 0;}