十二、C++11可变参数模版

📅 2026/7/12 22:03:59
十二、C++11可变参数模版
注这个是博主复习使用的专题仅适用于自己以及学习过C知识点的同学前言注这个是博主复习使用的专题仅适用于自己以及学习过C知识点的同学。一、可变模版参数1.1. 可变参数模版的概念可变参数模版是 C11 新增的特性之一它对参数高度泛化能够让我们创建可以接受可变参数的函数模版和类模版。在C11之前类模版和函数模版中只能包含固定数量的模版参数由于可变参数模版比较抽象使用起来需要一定的技巧。在C之前其也有可变参数的概念比如 printf 函数就可以接收任意多个参数但这是函数参数的可变参数并不是模版的可变参数。注本篇博客只讲解函数模板的可变参数。1.2. 可变参数模版的定义方法函数的可变参数模板定义方式如下templateclass...Args返回类型 函数名(Args...args){// 函数体}例如templateclass... Args void ShowList(Args... args) {}注用省略号来指出一个模版参数或函数参数的表示一个包在模版参数列表中class... 或typename... 指出接下来的参数表示0或多个类型列表在函数参数列表中类型名后面跟... 指出接下来表示0或者多个形参对象列表函数参数包可以用左值引用或右值引用表示。每个参数实例化都遵循引用折叠规则。现在调用ShowList函数时就可以传入任意多个参数了并且这些参数可以是不同类型的。比如int main() { ShowList(); ShowList(1); ShowList(1,A); ShowList(1,A,string(hello)); return 0; }我们可以在函数模板中通过sizeof计算参数包中参数的个数。比如templateclass... Args void ShowList(Args... args) { cout sizeof...(args) endl; } int main() { ShowList(); // 0 ShowList(1); // 1 ShowList(1,A); // 2 ShowList(1,A,string(hello)); // 3 return 0; }但是我们无法直接获取参数包中的每个参数只能通过展开参数包的方式来获取这是使用可变参数模版的一个主要特点也是最大的难点。他的本质有两种1. 结合引用折叠规则实例化出下面的函数。void ShowList(); void ShowList(int arg1); void ShowList(int arg1,char arg2); void ShowList(int arg1,char arg2,string arg3);2. 更本质的是生成下面的模版参数。void ShowList(); templateclass T1 void ShowList(T1 arg1); templateclass T1,class T2 void ShowList(T1 arg1,T2 arg2); templateclass T1, class T2,class T3 void ShowList(T1 arg1, T2 arg2,T3 arg3);注语法并不支持使用args[i]的方式来获取参数包中的参数templateclass... Args void ShowList(Args... args) { for (int i 0; i sizeof...(args); i) { cout args[i] ; } cout endl; }这个是错的。因此要获取参数包中的各个参数只能通过展开参数包的方式来获取一般我们会通过递归或逗号表达式来展开参数包。1.3. 参数包的展开方式1.3.1. 递归展开参数包递归展开参数包的方式如下给函数模版增加一个模版参数这样就可以从接收到的参数包中分离出一个参数出来。在函数模版中递归调用该函数模版调用时传入剩下的参数包。如此递归下去每次分离出参数包中的一个参数直到参数包中的所有参数都被取出来。例如打印调用函数时传入的各个参数那么函数模版可以这样子编写//展开函数 templateclass T, class ...Args void ShowList(T value, Args... args) { cout value ; //打印分离出的第一个参数 ShowList(args...); //递归调用将参数包继续向下传 }这时我们面临的问题就是如何终止函数的递归调用。编写无参的递归终止函数我们可以在刚才的基础上再编写一个无参的递归终止函数该函数的函数名与展开函数的函数名相同。如下//递归终止函数 void ShowList() { cout endl; } //展开函数 templateclass T, class ...Args void ShowList(T value, Args... args) { cout value ; //打印分离出的第一个参数 ShowList(args...); //递归调用将参数包继续向下传 } int main() { ShowList(); ShowList(1); ShowList(1, A); ShowList(1, A, string(hello)); return 0; }这样一来当递归调用 ShowList 函数模版时如果传入的参数包中的参数个数为0就会直接匹配到这个无参的递归终止函数就是递归的结束函数。当然也可以按照下面的写void ShowList() { cout endl; } template class T, class ...Args void ShowList(T x, Args... args) { cout x ; ShowList(args...); } // 编译时递归推导解析参数 template class ...Args void Print(Args... args) { ShowList(args...); } int main() { Print(1); Print(1, A); Print(1, A, string(hello)); return 0; }1.3.2. 逗号表达式展开参数包1. 通过列表获取参数包中的参数如果参数包中各个参数的类型都是整型那么也可以把这个参数包放到列表当中初始化这个整型数组此时参数包中参数就放到数组中了。比如templateclass...Args void Print(Args...args) { int arr[] { args...}; for (auto e : arr) { cout e ; } cout endl; } int main() { Print(1); Print(1,2); Print(1,2,3); return 0; }但C并不像Python这样的语言C规定一个容器中存储的数据类型必须是相同的因此如果这样写的话那么调用ShowList函数时传入的参数只能是整型的并且还不能传入0个参数因为数组的大小不能为0因此我们还需要在此基础上借助逗号表达式来展开参数包。2. 通过逗号表达式展开参数包虽然我们不能用不同类型的参数去初始化一个整型数组但我们可以借助逗号表达式。逗号表达式会从左到右依次计算各个表达式并且将最后一个表达式的值作为返回值进行返回。将逗号表达式的最后一个表达式设置为一个整型值确保逗号表达式返回的是一个整型值。将处理参数包中参数的动作封装成一个函数将该函数的调用作为逗号表达式的第一个表达式。这样一来在执行逗号表达式时就会先调用处理函数处理对应的参数然后再将逗号表达式中的最后一个整型值作为返回值来初始化整型数组。比如templateclass T const T GetArg(const T x) { cout x ; return x; } templateclass...Args void Arguments(Args... args) {} templateclass... Args void Print(Args... args) { Arguments(GetArg(args)...); } int main() { Print(1, string(2345), 1.1); return 0; }他的本质其实是编译器在编译的是偶实例化下面的函数templateclass T const T GetArg(const T x) { cout x ; return x; } templateclass...Args void Arguments(Args... args) {} templateclass... Args void Print(int x,string y,double z) { Arguments(GetArg(x),GetArg(y),GetArg(z)); } int main() { Print(1, string(2345), 1.1); return 0; }1.4. STL容器汇总的emplace相关接口函数C11标准给STL中的容器增加emplace版本的插入接口比如list容器的push_front、push_back和insert函数都增加了对应的emplace_front、emplace_back和emplace函数。如下注意emplace系列接口的可变模板参数类型都带有“”这个表示的是万能引用而不是右值引用。emplace系列接口的使用方式emplace系列接口的使用方式与容器原有的插入接口的使用方式类似但又有一些不同之处。以list容器的emplace_back和push_back为例调用push_back函数插入元素时可以传入左值对象或者右值对象也可以使用列表进行初始化。调用emplace_back函数插入元素时也可以传入左值对象或者右值对象但不可以使用列表进行初始化。除此之外emplace系列接口最大的特点就是插入元素时可以传入用于构造元素的参数包。emplace系列接口的意义由于emplace系列接口的可变模版参数类型都是万能引用因此既可以接收左值对象也可以接收右值对象还可以接收参数包。如果调用emplace系列接口时传入的是左值对象那么首先需要先在此之前调用构造函数实例化出一个左值对象最终在使用定位new表达式调用构造函数对空间进行初始化时会匹配到拷贝构造函数。如果调用emplace系列接口时传入的是右值对象那么就需要在此之前调用构造函数实例化出一个右值对象最终在使用定位new表达式调用构造函数对空间进行初始化时就会匹配到移动构造函数。如果调用emplace系列接口时传入的是参数包那么就可以直接调用函数进行插入并且最终在使用定位new表达式调用构造函数对空间进行初始化匹配到的是构造函数。总结一下传入左值对象需要调用构造函数 拷贝构造函数。传入右值对象需要调用构造函数 移动构造函数。传入参数包只需要调用构造函数。当然这里的前提是容器中存储的元素所对应的类是一个需要深拷贝的类并且该类实现了移动构造函数。否则调用emplace系列接口时传入左值对象和传入右值对象的效果都是一样的都需要调用一次构造函数和一次拷贝构造函数。实际emplace系列接口的一部分功能和原有各个容器插入接口是重叠的因为容器原有的push_back、push_front和insert函数也提供了右值引用版本的接口如果调用这些接口时如果传入的是右值对象那么最终也是会调用对应的移动构造函数进行资源的移动的。emplace接口的意义emplace系列接口最大的特点就是支持传入参数包用这些参数包直接构造出对象这样就能减少一次拷贝这就是为什么有人说emplace系列接口更高效的原因。但emplace系列接口并不是在所有场景下都比原有的插入接口高效如果传入的是左值对象或右值对象那么emplace系列接口的效率其实和原有的插入接口的效率是一样的。emplace系列接口真正高效的情况是传入参数包的时候直接通过参数包构造出对象避免了中途的一次拷贝。验证如果要验证我们上述对emplace系列接口的说法需要借助一个深拷贝的类下面模拟实现了一个简化版的string类类当中只编写了我们需要用到的成员函数。namespace xxhh { class string { public: //构造函数 string(const char* str ) { cout string(const char* str) -- 构造函数 endl; _size strlen(str); //初始时字符串大小设置为字符串长度 _capacity _size; //初始时字符串容量设置为字符串长度 _str new char[_capacity 1]; //为存储字符串开辟空间多开一个用于存放\0 strcpy(_str, str); //将C字符串拷贝到已开好的空间 } //交换两个对象的数据 void swap(string s) { //调用库里的swap ::swap(_str, s._str); //交换两个对象的C字符串 ::swap(_size, s._size); //交换两个对象的大小 ::swap(_capacity, s._capacity); //交换两个对象的容量 } //拷贝构造函数现代写法 string(const string s) :_str(nullptr) , _size(0) , _capacity(0) { cout string(const string s) -- 拷贝构造 endl; string tmp(s._str); //调用构造函数构造出一个C字符串为s._str的对象 swap(tmp); //交换这两个对象 } //移动构造 string(string s) :_str(nullptr) , _size(0) , _capacity(0) { cout string(string s) -- 移动构造 endl; swap(s); } //拷贝赋值函数现代写法 string operator(const string s) { cout string operator(const string s) -- 深拷贝 endl; string tmp(s); //用s拷贝构造出对象tmp swap(tmp); //交换这两个对象 return *this; //返回左值支持连续赋值 } //移动赋值 string operator(string s) { cout string operator(string s) -- 移动赋值 endl; swap(s); return *this; } //析构函数 ~string() { //delete[] _str; //释放_str指向的空间 _str nullptr; //及时置空防止非法访问 _size 0; //大小置0 _capacity 0; //容量置0 } private: char* _str; size_t _size; size_t _capacity; }; }由于我们在string的构造函数、拷贝构造函数和移动构造函数当中均打印了一条提示语句因此我们可以通过控制台输出来判断这些函数是否被调用。下面我们用一个容器来存储模拟实现的string并以不同的传参形式调用emplace系列函数。比如#includelist int main() { listpairint, xxhh::string mylist; pairint, xxhh::string kv(1, one); mylist.emplace_back(kv); //传左值 cout endl; mylist.emplace_back(pairint, xxhh::string(2, two)); //传右值 cout endl; mylist.emplace_back(3, three); //传参数包 return 0; }完