☰
【C++】C++11可变参数模板/移动构造/移动赋值
2026/10/22 7:25:05 网站建设 项目流程

1可变参数模板

1.1基本语法及原理

• C++11⽀持可变参数模板,也就是说⽀持可变数量参数的函数模板和类模板,可变数⽬的参数被称为参数包,存在两种参数包:模板参数包,表⽰零或多个模板参数;函数参数包:表⽰零或多个函数参数。
• template <class …Args> void Func(Args… args) {}
• template <class …Args> void Func(Args&… args) {}
• template <class …Args> void Func(Args&&… args) {}
• 我们⽤省略号来指出⼀个模板参数或函数参数的表⽰⼀个包,在模板参数列表中,class…或typename…指出接下来的参数表⽰零或多个类型列表;在函数参数列表中,类型名后⾯跟…指出接下来表⽰零或多个形参对象列表;函数参数包可以⽤左值引⽤或右值引⽤表⽰,跟前⾯普通模板⼀样,每个参数实例化时遵循引⽤折叠规则。
• 可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
• 这⾥我们可以使⽤sizeof…运算符去计算参数包中参数的个数。

template<class...Args>voidPrint(Args&&...args){cout<<sizeof...(args)<<endl;}intmain(){doublex=2.2;Print();// 包⾥有0个参数Print(1);// 包⾥有1个参数Print(1,string("xxxxx"));// 包⾥有2个参数Print(1.1,string("xxxxx"),x);// 包⾥有3个参数return0;}// 原理1:编译本质这⾥会结合引⽤折叠规则实例化出以下四个函数voidPrint();voidPrint(int&&arg1);voidPrint(int&&arg1,string&&arg2);voidPrint(double&&arg1,string&&arg2,double&arg3);// 原理2:更本质去看没有可变参数模板,我们实现出这样的多个函数模板才能⽀持// 这⾥的功能,有了可变参数模板,我们进⼀步被解放,他是类型泛化基础// 上叠加数量变化,让我们泛型编程更灵活。voidPrint();template<classT1>voidPrint(T1&&arg1);template<classT1,classT2>voidPrint(T1&&arg1,T2&&arg2);template<classT1,classT2,classT3>voidPrint(T1&&arg1,T2&&arg2,T3&&arg3);// ...

1.2 包扩展

• 对于⼀个参数包,我们除了能计算他的参数个数,我们能做的唯⼀的事情就是扩展它,当扩展⼀个包时,我们还要提供⽤于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元素应⽤模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(…)来触发扩展操作。底层的实现细节如图1所⽰。
• C++还⽀持更复杂的包扩展,直接将参数包依次展开依次作为实参给⼀个函数去处理。

// 可变模板参数// 参数类型可变// 参数个数可变// 打印参数包内容//template <class ...Args>//void Print(Args... args)//{// // 可变参数模板编译时解析// // 下⾯是运⾏获取和解析,所以不⽀持这样⽤// cout << sizeof...(args) << endl;// for (size_t i = 0; i < sizeof...(args); i++)// {// cout << args[i] << " ";// }// cout << endl;//}voidShowList(){// 编译器时递归的终⽌条件,参数包是0个时,直接匹配这个函数cout<<endl;}template<classT,class...Args>voidShowList(T x,Args...args){cout<<x<<" ";// args是N个参数的参数包// 调⽤ShowList,参数包的第⼀个传给x,剩下N-1传给第⼆个参数包ShowList(args...);}// 编译时递归推导解析参数template<class...Args>voidPrint(Args...args){ShowList(args...);}intmain(){Print();Print(1);Print(1,string("xxxxx"));Print(1,string("xxxxx"),2.2);return0;}//template <class T, class ...Args>//void ShowList(T x, Args... args)//{// cout << x << " ";// Print(args...);//}// Print(1, string("xxxxx"), 2.2);调⽤时// 本质编译器将可变参数模板通过模式的包扩展,编译器推导的以下三个重载函数函数//void ShowList(double x)//{// cout << x << " ";// ShowList();//}////void ShowList(string x, double z)//{// cout << x << " ";// ShowList(z);//}////void ShowList(int x, string y, double z)//{// cout << x << " ";// ShowList(y, z);//}//void Print(int x, string y, double z)//{// ShowList(x, y, z);//}template<classT>constT&GetArg(constT&x){cout<<x<<" ";returnx;}template<class...Args>voidArguments(Args...args){}template<class...Args>voidPrint(Args...args){// 注意GetArg必须返回或者到的对象,这样才能组成参数包给ArgumentsArguments(GetArg(args)...);}// 本质可以理解为编译器编译时,包的扩展模式// 将上⾯的函数模板扩展实例化为下⾯的函数// 是不是很抽象,C++11以后,只能说委员会的⼤佬设计语法思维跳跃得太厉害//void Print(int x, string y, double z)//{// Arguments(GetArg(x), GetArg(y), GetArg(z));//}intmain(){Print(1,string("xxxxx"),2.2);return0;}

4.3 empalce系列接⼝

• template <class… Args> void emplace_back (Args&&… args);
• template <class… Args> iterator emplace (const_iterator position,Args&&… args);
• C++11以后STL容器新增了empalce系列的接⼝,empalce系列的接⼝均为模板可变参数,功能上兼容push和insert系列,但是empalce还⽀持新玩法,假设容器container,empalce还⽀持直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象。
• emplace_back总体⽽⾔是更⾼效,推荐以后使⽤emplace系列替代insert和push系列
• 第⼆个程序中我们模拟实现了list的emplace和emplace_back接⼝,这⾥把参数包不段往下传递,最终在结点的构造中直接去匹配容器存储的数据类型T的构造,所以达到了前⾯说的empalce⽀持直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象。
• 传递参数包过程中,如果是 Args&&… args 的参数包,要⽤完美转发参数包,⽅式如下
std::forward(args)… ,否则编译时包扩展后右值引⽤变量表达式就变成了左
值。

#include<list>// emplace_back总体⽽⾔是更⾼效,推荐以后使⽤emplace系列替代insert和push系列intmain(){list<bit::string>lt;// 传左值,跟push_back⼀样,⾛拷⻉构造bit::strings1("111111111111");lt.emplace_back(s1);cout<<"*********************************"<<endl;// 右值,跟push_back⼀样,⾛移动构造lt.emplace_back(move(s1));cout<<"*********************************"<<endl;// 直接把构造string参数包往下传,直接⽤string参数包构造string// 这⾥达到的效果是push_back做不到的lt.emplace_back("111111111111");cout<<"*********************************"<<endl;list<pair<bit::string,int>>lt1;// 跟push_back⼀样// 构造pair + 拷⻉/移动构造pair到list的节点中data上pair<bit::string,int>kv("苹果",1);lt1.emplace_back(kv);cout<<"*********************************"<<endl;// 跟push_back⼀样lt1.emplace_back(move(kv));cout<<"*********************************"<<endl;////////////////////////////////////////////////////////////////////// 直接把构造pair参数包往下传,直接⽤pair参数包构造pair// 这⾥达到的效果是push_back做不到的lt1.emplace_back("苹果",1);cout<<"*********************************"<<endl;return0;}// List.hnamespacebit{template<classT>structListNode{ListNode<T>*_next;ListNode<T>*_prev;T _data;ListNode(T&&data):_next(nullptr),_prev(nullptr),_data(move(data)){}template<class...Args>ListNode(Args&&...args):_next(nullptr),_prev(nullptr),_data(std::forward<Args>(args)...){}};template<classT,classRef,classPtr>structListIterator{typedefListNode<T>Node;typedefListIterator<T,Ref,Ptr>Self;Node*_node;ListIterator(Node*node):_node(node){}// ++it;Self&operator++(){_node=_node->_next;return*this;}Self&operator--(){_node=_node->_prev;return*this;}Refoperator*(){return_node->_data;}booloperator!=(constSelf&it){return_node!=it._node;}};template<classT>classlist{typedefListNode<T>Node;public:typedefListIterator<T,T&,T*>iterator;typedefListIterator<T,constT&,constT*>const_iterator;iteratorbegin(){returniterator(_head->_next);}iteratorend(){returniterator(_head);}voidempty_init(){_head=newNode();_head->_next=_head;_head->_prev=_head;}list(){empty_init();}voidpush_back(constT&x){insert(end(),x);}voidpush_back(T&&x){insert(end(),move(x));}iteratorinsert(iterator pos,constT&x){Node*cur=pos._node;Node*newnode=newNode(x);Node*prev=cur->_prev;// prev newnode curprev->_next=newnode;newnode->_prev=prev;newnode->_next=cur;cur->_prev=newnode;returniterator(newnode);}iteratorinsert(iterator pos,T&&x){Node*cur=pos._node;Node*newnode=newNode(move(x));Node*prev=cur->_prev;// prev newnode curprev->_next=newnode;newnode->_prev=prev;newnode->_next=cur;cur->_prev=newnode;returniterator(newnode);}template<class...Args>voidemplace_back(Args&&...args){insert(end(),std::forward<Args>(args)...);}// 原理:本质编译器根据可变参数模板⽣成对应参数的函数/*void emplace_back(string& s) { insert(end(), std::forward<string>(s)); } void emplace_back(string&& s) { insert(end(), std::forward<string>(s)); } void emplace_back(const char* s) { insert(end(), std::forward<const char*>(s)); } */template<class...Args>iteratorinsert(iterator pos,Args&&...args){Node*cur=pos._node;Node*newnode=newNode(std::forward<Args>(args)...);Node*prev=cur->_prev;// prev newnode curprev->_next=newnode;newnode->_prev=prev;newnode->_next=cur;cur->_prev=newnode;returniterator(newnode);}private:Node*_head;};}// Test.cpp#include"List.h"// emplace_back总体⽽⾔是更⾼效,推荐以后使⽤emplace系列替代insert和push系列intmain(){bit::list<bit::string>lt;// 传左值,跟push_back⼀样,⾛拷⻉构造bit::strings1("111111111111");lt.emplace_back(s1);cout<<"*********************************"<<endl;// 右值,跟push_back⼀样,⾛移动构造lt.emplace_back(move(s1));cout<<"*********************************"<<endl;// 直接把构造string参数包往下传,直接⽤string参数包构造string// 这⾥达到的效果是push_back做不到的lt.emplace_back("111111111111");cout<<"*********************************"<<endl;bit::list<pair<bit::string,int>>lt1;// 跟push_back⼀样// 构造pair + 拷⻉/移动构造pair到list的节点中data上pair<bit::string,int>kv("苹果",1);lt1.emplace_back(kv);cout<<"*********************************"<<endl;// 跟push_back⼀样lt1.emplace_back(move(kv));cout<<"*********************************"<<endl;////////////////////////////////////////////////////////////////////// 直接把构造pair参数包往下传,直接⽤pair参数包构造pair// 这⾥达到的效果是push_back做不到的lt1.emplace_back("苹果",1);cout<<"*********************************"<<endl;return0;}

2新的类功能

2.1默认的移动构造和移动赋值

• 原来C++类中,有6个默认成员函数:构造函数/析构函数/拷⻉构造函数/拷⻉赋值重载/取地址重载/const取地址重载,最后重要的是前4个,后两个⽤处不⼤,默认成员函数就是我们不写编译器会⽣成⼀个默认的。C++11新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
• 如果你==没有⾃⼰实现移动构造函数,且没有实现析构函数、拷⻉构造、拷⻉赋值重载中的任意⼀个。==那么编译器会⾃动⽣成⼀个默认移动构造。默认⽣成的移动构造函数,对于内置类型成员会执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调⽤移动构造,没有实现就调⽤拷⻉构造。
• 如果你没有⾃⼰实现移动赋值重载函数,且没有实现析构函数、拷⻉构造、拷⻉赋值重载中的任意⼀个,那么编译器会⾃动⽣成⼀个默认移动赋值。默认⽣成的移动构造函数,对于内置类型成员会执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调⽤移动赋值,没有实现就调⽤拷⻉赋值。(默认移动赋值跟上⾯移动构造完全类似)
• 如果你提供了移动构造或者移动赋值,编译器不会⾃动提供拷⻉构造和拷⻉赋值。

classPerson{public:Person(constchar*name="",intage=0):_name(name),_age(age){}/*Person(const Person& p) :_name(p._name) ,_age(p._age) {}*//*Person& operator=(const Person& p) { if(this != &p) { _name = p._name; _age = p._age; } return *this; }*//*~Person() {}*/private:bit::string _name;int_age;};intmain(){Person s1;Person s2=s1;Person s3=std::move(s1);Person s4;s4=std::move(s2);return0;}

2.2 委托构造函数与继承构造函数

委托构造函数
C++中的委托构造函数是C++11引入的一个特性,允许一个构造函数调用同类中的其他构造函数,从而减少代码重复并提高可维护性
被委托的构造函数必须初始化全部成员变量,因为委托构造函数不能再重复初始化
继承构造函数(本质:让编译器自己生成派生类对象对基类参数的构造)
C++11引入的新特性,允许派生类直接继承基类的构造函数
派生类继承基类的普通构造函数,特殊的拷贝构造/移动构造函数不继承
继承构造中派生类自己的成员变量如果有缺省值会使用缺省值初始化,如果没有,内置成员不确定,自定义类型成员使用默认构造初始化

2.3 成员变量声明时给缺省值

成员变量声明时给缺省值是给初始化列表⽤的,如果没有显⽰在初始化列表初始化,就会在初始化列表⽤这个却绳⼦初始化,这个我们在类和对象部分讲过了,忘了就去复习吧。

2.4defult和delete

• C++11可以让你更好的控制要使⽤的默认函数。假设你要使⽤某个默认的函数,但是因为⼀些原因这个函数没有默认⽣成。⽐如:我们提供了拷⻉构造,就不会⽣成移动构造了,那么我们可以使⽤default关键字显⽰指定移动构造⽣成。
• 如果能想要限制某些默认函数的⽣成,在C++98中,是该函数设置成private,并且只声明补丁,这样只要其他⼈想要调⽤就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指⽰编译器不⽣成对应函数的默认版本,称=delete修饰的函数为删除函数。

classPerson{public:Person(constchar*name="",intage=0):_name(name),_age(age){}Person(constPerson&p):_name(p._name),_age(p._age){}Person(Person&&p)=default;//Person(const Person& p) = delete;private:bit::string _name;int_age;};intmain(){Person s1;Person s2=s1;Person s3=std::move(s1);return0;}

2.4 final与override

这个我们在继承和多态章节已经进⾏了详细讲过了,忘了就去复习吧。

3. STL中⼀些变化

• 下图1圈起来的就是STL中的新容器,但是实际最有⽤的是unordered_map和unordered_set。这两个我们前⾯已经进⾏了⾮常详细的讲解,其他的⼤家了解⼀下即可。
• STL中容器的新接⼝也不少,最重要的就是右值引⽤和移动语义相关push/insert/emplace系列接⼝和移动构造和移动赋值,还有initializer_list版本的构造等,这些前⾯都讲过了,还有⼀些⽆关痛痒的如cbegin/cend等需要时查查⽂档即可。
• 容器的范围for遍历,这个在容器部分也讲过了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询