【C++11】深入解析C++可变参数模板
可变参数模板序一、基本语法与原理二、包扩展1. 参数包展开一——递归展开2. 参数包展开二——利用函数返回值3. 参数包展开三——初始化列表三、emplace情况一左值情况二右值情况三直接传参情况四多参数类型场景序在之前如果我们想要实现一个函数可以接收不同类型的参数我们可以通过模板来实现那如果想要它接收不同数量的参数怎么办呢在以前我们可以通过函数重载来实现但是这种方法非常麻烦要写多个函数。于是C11引入了可变参数模板。一、基本语法与原理可变参数模板顾名思义就是支持可变数量的函数模板或类模板即可以传不同数量的参数可变数量的参数被称为参数包参数包分为模板参数包和函数参数包。它们都能支持传入零到多个函数参数。经典形式template class ...Args void Func(Args... args) {} //左值引用 template class ...Args void Func(Args... args) {} //万能引用 template class ...Args void Func(Args... args) {}我们用省略号来指出⼀个模板参数或函数参数的表示一个包在模板参数列表中class...或typename...指出接下来的参数表示零或多个类型列表在函数参数列表中类型名后面跟…指出接下来表示零或多个形参对象列表函数参数包可以用左值引用或右值引用表示跟前面普通模板一样每个参数实例化时遵循引用折叠规则。上面的Args不是一个类型是一组类型。可变参数模板的原理跟模板类似本质还是去实例化对应类型和个数的多个函数。我们可以使用运算符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; }可变参数模板是在编译时就确定了参数的类型上面的调用会结合引用折叠规则实例化出以下四个函数void Print(); void Print(int arg1); void Print(int arg1, string arg2); void Print(double arg1, string arg2, double arg3);二、包扩展当我们传入参数包时。我们是无法直接去使用其中的变量的此时我们就引出包扩展以使用包中的参数。1. 参数包展开一——递归展开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...); }我们来看看它的递归展开图再来看一段代码template class T, class ...Args void ShowList(T x, Args... args) { if(sizeof...(args)0) return; cout x ; // args是N个参数的参数包 // 调⽤ShowList参数包的第⼀个传给x剩下N-1传给第⼆个参数包 ShowList(args...); } // 编译时递归推导解析参数 template class ...Args void Print(Args... args) { ShowList(args...); }这是一个典型的错误可变参数模板的参数匹配是在编译时进行的而ShowList中的if语句是在参数确定之后才执行的是运行时逻辑当args是零个参数时无法匹配到函数。2. 参数包展开二——利用函数返回值template class T const T GetArg(const T x) { cout x ; return x; } template class ...Args void Arguments(Args... args) {} template class ...Args void Print(Args... args) { // 注意GetArg必须返回获得到的对象这样才能组成参数包给Arguments Arguments(GetArg(args)...); } int main() { Print(1, 2.2, string(hello)); return 0; }当我们调用Print时传入的args是int, double, const char*Print内...在GetArg(args)后面因此展开后得到Arguments( GetArg(1), GetArg(2.5), GetArg(hello) );然后它就会依次调用GetArgs依次输出。结果我们看到结果好像反了一下。这是为什么呢其实像上面的Arguments函数这样的参数为函数返回值的函数编译器并没有准确规定一定要先执行谁后执行谁这全取决于编译器的实现。如果我们想要正序的输出可以用第三种初始化列表的方法。3. 参数包展开三——初始化列表templateclass... Args void Print(Args... args) { int arr[] { (cout args ,0)... }; cout endl; } int main() { Print(1, 2.2, string(hello)); return 0; }我们借助了一个数组来依次执行输出它的值是一个逗号表达式表达式会先执行第一个参数cout然后再返回第二个参数0刚好让数组接收...前面就是这个表达式因此将其展开后得到int arr[] { (cout 1 , 0), (cout 2.2 , 0), (cout string(hello) , 0) };随后arr的值就是[0,0,0]。这样就能保证编译器依次执行了。三、emplace我们以list的emplace_back为例来学习emplace系列接口。先看定义可以看到list::emplace_back是用万能引用和可变参数模板来实现的。同样都是插入元素emplace_back与push_back有什么区别呢情况一左值int main() { listzzt::string lt; zzt::string s1 1111111111; cout ******************************** endl; lt.push_back(s1); cout ******************************** endl; lt.emplace_back(s1); return 0; }输出可见传左值时两者没有任何区别。情况二右值int main() { listzzt::string lt; zzt::string s1 1111111111; zzt::string s2 1111111111; cout ******************************** endl; lt.push_back(move(s1)); cout ******************************** endl; lt.emplace_back(move(s2)); return 0; }输出可见传右值时两者依旧完全相同。情况三直接传参int main() { listzzt::string lt; lt.push_back(1111111111); cout ******************************** endl; lt.emplace_back(1111111111); }输出此时就不一样了push_back是构造移动构造emplace_back是直接构造。我们先来看看两者声明的区别push_back:void push_back (const value_type val); void push_back (value_type val);emplace_back:template class... Args void emplace_back (Args... args);对于push_back1111111111是const char*类型当我们传入1111111111时由于push_back中的value_type在定义listzzt::string lt时就已经确定为zzt::string了因此编译器会先构造一个zzt::string类型的临时对象再移动构造给val。对于emplace_back它的参数类型是在编译时展开确定的因此传入const char*它的参数类型就是const char*然后再进行一系列传递直至传到构造函数直接构造。实际上两者效率几乎相同因为移动构造的开销非常非常小。情况四多参数类型场景就以pair为例int main() { listpairzzt::string, int lt; lt.push_back({ 苹果,1 }); cout ******************************** endl; lt.emplace_back(苹果, 1); return 0; }输出这个也是不同的原理和上面的差不多。对于push_back它只能通过花括号传参不能直接像emplace_back一样通过多个参数来传参。对于emplace_back它只能通过多个参数传参不能使用花括号因为可变参数模板是在编译时确定类型的传入一个花括号它不知道一个花括号代表什么无法推出参数类型。总结部分场景下emplace可以直接构造push和insert是构造移动构造或构造拷贝构造在效率方面emplace略胜一筹emplace综合而言更好用、更强大因此使用时推荐用emplace替代push和insert。