C++11(可变参数模板、新的类功能和STL中的一些变化)
C++11(可变参数模板、新的类功能和STL中的一些变化)
- 1. 可变参数模板
- 1.1 基本语法及原理
- 1.2 包扩展
- 1.3 emplace系列接口
- 2. 新的类功能
- 2.1 默认的移动构造和移动赋值
- 2.2 成员变量声明时给缺省值
- 2.3 default和delete
- 2.4 final和override
- 3. STL中的一些变化
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>
void Print(Args&&... args)
{cout << sizeof...(args) << endl; // sizeof...运算符编译时计算参数包中参数的个数
}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;
} // 原理1:编译器本质这⾥会结合引⽤折叠规则实例化出以下四个函数
void Print();
void Print(int&& arg1);
void Print(int&& arg1, string&& arg2);
void Print(double&& arg1, string&& arg2, double& arg3);// 原理2:更本质去看,如果没有可变参数模板,我们要实现出这样的多个函数模板才能⽀持
// 这⾥的功能,它是类型泛化基础上叠加数量变化,让我们泛型编程更灵活。
void Print();template <class T1>
void Print(T1&& arg1);template <class T1, class T2>
void Print(T1&& arg1, T2&& arg2);template <class T1, class T2, class T3>
void Print(T1&& arg1, T2&& arg2, T3&& arg3);
// ...
1.2 包扩展
- 对于⼀个参数包,我们除了能计算它的参数个数,我们能做的唯⼀的事情就是扩展它,当扩展⼀个包时,我们还要提供⽤于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元素应⽤模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(…)来触发扩展操作。底层的实现细节如下图所示。
- C++还⽀持更复杂的包扩展,直接将参数包依次展开依次作为实参给⼀个函数去处理。
// 可变模板参数
// 参数类型可变
// 参数个数可变
// 打印参数包内容
template <class ...Args>
void Print(Args... args)
{// 可变参数模板编译时解析// 下⾯是运⾏获取和解析,所以不⽀持这样⽤cout << sizeof...(args) << endl;//errfor (size_t i = 0; i < sizeof...(args); i++){cout << args[i] << " ";}cout << endl;
}
要递归推演的包扩展
void ShowList()
{// 编译时递归的终止条件cout << endl;
}//err
//template <class T, class ...Args>
//void ShowList(T x, Args... args)
//{
// cout << x << " ";
//
// // 运行时条件判定,所以不能这样写
// if (sizeof...(args) == 0)
// return;
//
// ShowList(args...); // 编译时递归
//}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;
}
void ShowList()
{cout << endl;
}void ShowList(double x)
{cout << x << " ";ShowList();
}void ShowList(string x, double x3)
{cout << x << " ";ShowList(x3);
}void ShowList(int x, string x2, double x3)
{cout << x << " ";ShowList(x2, x3);
}void Print(int x1, string x2, double x3)
{ShowList(x1, x2, x3);
}int main()
{Print(1, string("xxxxx"), 2.2);return 0;
}
不用递归推演的包扩展
template <class T>
const T& GetArg(const T& x)
{cout << x << " ";return x;
} //template <class T>
//int GetArg(const T& x)
//{
// cout << x << " ";
// // GetArg返回什么不重要,只要返回N个参数就行
// // 因为返回的参数在Arguments中什么也不干
// return 0;
//}template <class ...Args>
void Arguments(Args... args)
{}template <class ...Args>
void Print(Args... args)
{// 注意GetArg必须返回获得到的对象,这样才能组成参数包给Arguments// GetArg的返回值组成实参参数包,传给ArgumentsArguments(GetArg(args)...); // 这个不用递归推演,就是包扩展
}// 本质可以理解为编译器编译时,包的扩展模式将上⾯的函数模板扩展实例化为下⾯的函数
//void Print(int x, string y, double z)
//{
// Arguments(GetArg(x), GetArg(y), GetArg(z));
//}int main()
{Print(1, string("xxxxx"), 2.2);return 0;
}
为什么是倒序打印?
函数调用参数求值顺序:
在C++标准中,函数参数的求值顺序是未指定的
大多数编译器的实际实现是从右到左求值
所以实际执行可能是:GetArg(2.2)→ GetArg(string(“xxxxx”))→ GetArg(1)
1.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和insert系列,但是empalce还⽀持新玩法,假设容器为container< T >,empalce还⽀持直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象。
- emplace_back总体⽽⾔是更⾼效,推荐以后使⽤emplace系列替代insert和push系列
- 第⼆个程序中我们模拟实现了list的emplace和emplace_back接⼝,这⾥把参数包不段往下传递,最终在结点的构造中直接去匹配容器存储的数据类型T的构造,所以达到了前⾯说的empalce⽀持直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象。
- 传递参数包过程中,如果是Args&&… args 的参数包,要⽤完美转发参数包,⽅式如下std::forward< Args >(args)… ,否则编译时包扩展后右值引⽤变量表达式就变成了左值。
// emplace系列总体而言更高效,推荐以后使用emplace系列替代insert和push系列
int main()
{std::list<bs::string> lt1;// 传左值,跟push_back一样,走拷贝构造bs::string s1("1111111111111");lt1.push_back(s1);lt1.emplace_back(s1);cout << "**********************************" << endl;// 传右值,跟push_back一样,走移动构造bs::string s2("11111111111111111");lt1.push_back(move(s2));bs::string s3("111111111111111111");lt1.emplace_back(move(s3));cout << "**********************************" << endl;// emplace_back的效率略高一筹lt1.push_back("1111111111111111111"); // 单参数构造的隐式类型转换// 直接把构造string参数包往下传,直接用string参数包构造stringlt1.emplace_back("1111111111111111111");cout << "**********************************" << endl;std::list<pair<bs::string, int>> lt2;// 传左值,跟push_back一样,走拷贝构造pair<bs::string, int> kv1("11111111111", 1);lt2.push_back(kv1);lt2.emplace_back(kv1);cout << "**********************************" << endl;// 传右值,跟push_back一样,走移动构造pair<bs::string, int> kv2("11111111111", 1);lt2.push_back(move(kv2));pair<bs::string, int> kv3("11111111111", 1);lt2.emplace_back(move(kv3));cout << "**********************************" << endl;// emplace_back的效率略高一筹lt2.push_back({ "1111111111111111111", 1 }); // 多参数构造的隐式类型转换// 花括号{},编译器会识别为initializer list,而initializer list的参数类型要相同// 所以编译器报错: “initializer list”: 不是“_Valty”的有效模板参数// lt2.emplace_back({ "1111111111111111111", 1 }); // 不支持// 参数包传下去,最后直接构造容器上的对象lt2.emplace_back("1111111111111111111", 1);cout << "**********************************" << endl;return 0;
}
//list.h
#pragma once//list.h
// 无关接口删除了
namespace bs
{template<class T>struct list_node{T _data;list_node<T>* _next = nullptr;list_node<T>* _prev = nullptr;//list_node(const T& val)// :_data(val)// , _next(nullptr)// , _prev(nullptr)//{}//// insert中的val传到这//list_node(T&& val = T())// // 再把val强转成右值属性// // val会去调用bs:string的移动构造// :_data(move(val))// , _next(nullptr)// , _prev(nullptr)//{}template<class... Args>list_node(Args&&... args): _data(forward<Args>(args)...), _next(nullptr), _prev(nullptr){}list_node() = default;// 这里写万能引用效果不好// 得加一个默认构造,因为我们在new Node时,// 函数模板的X是泛型,还未实例化template<class X>list_node(X&& val):_data(forward<X>(val)), _next(nullptr), _prev(nullptr){}};template<class T, class Ref, class Ptr>struct __list_iterator{typedef list_node<T> Node;typedef __list_iterator<T, Ref, Ptr> Self;Node* _node;__list_iterator(Node* node):_node(node){}Ref operator*(){return _node->_data;}Self& operator++(){_node = _node->_next;return *this;}bool operator!=(const Self& it) const{return _node != it._node;}};template<class T>class list{typedef list_node<T> Node;public:typedef __list_iterator<T, T&, T*> iterator;typedef __list_iterator<T, const T&, const T*> const_iterator;iterator begin(){return iterator(_head->_next);}iterator end(){return iterator(_head);}void empty_init(){_head = new Node;_head->_next = _head;_head->_prev = _head;}list(){empty_init();}//// 左值引用//void push_back(const T& x)//{// insert(end(), x);//}//// 右值引用//void push_back(T&& x)//{// insert(end(), move(x));//}// 万能引用template<class X>void push_back(X&& x){insert(end(), forward<X>(x));}template<class... Args>void emplace_back(Args&&... args){emplace(end(), forward<Args>(args)...);}template<class... Args>iterator emplace(iterator pos, Args&&... args){Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(forward<Args>(args)...);//prev newnode curprev->_next = newnode;newnode->_next = cur;cur->_prev = newnode;newnode->_prev = prev;++_size;//返回新插入节点位置的迭代器return iterator(newnode);}//iterator insert(iterator pos, const T& val)//{// Node* cur = pos._node;// Node* prev = cur->_prev;// Node* newnode = new Node(val);// //prev newnode cur// prev->_next = newnode;// newnode->_next = cur;// cur->_prev = newnode;// newnode->_prev = prev;// ++_size;// //返回新插入节点位置的迭代器// return iterator(newnode);//}//// 假设val具有右值属性,那val应该具有常性,不能改变//// 但是移动构造/移动赋值需要改变它,所以val必须具有左值属性//iterator insert(iterator pos, T&& val)//{// Node* cur = pos._node;// Node* prev = cur->_prev;// // 既然val具有左值属性,那么要想调用移动构造// // 要把val强转成右值属性传过去才行// Node* newnode = new Node(move(val));// //prev newnode cur// prev->_next = newnode;// newnode->_next = cur;// cur->_prev = newnode;// newnode->_prev = prev;// ++_size;// //返回新插入节点位置的迭代器// return iterator(newnode);//}// X的类型是实参传递给形参推出来的// 万能引用template<class X>iterator insert(iterator pos, X&& val){Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(forward<X>(val));//prev newnode curprev->_next = newnode;newnode->_next = cur;cur->_prev = newnode;newnode->_prev = prev;++_size;//返回新插入节点位置的迭代器return iterator(newnode);}private:Node* _head;size_t _size = 0;};
}
// 换成我们自己写的list,方便观察
#include "list.h"int main()
{bs::list<bs::string> lt1; // 我们自己写的有一个哨兵位cout << "**********************************" << endl;// 传左值,跟push_back一样,走拷贝构造bs::string s1("1111111111111");lt1.push_back(s1);lt1.emplace_back(s1);cout << "**********************************" << endl;// 传右值,跟push_back一样,走移动构造bs::string s2("11111111111111111");lt1.push_back(move(s2));bs::string s3("111111111111111111");lt1.emplace_back(move(s3));cout << "**********************************" << endl;// emplace_back的效率略高一筹lt1.push_back("1111111111111111111"); // 单参数构造的隐式类型转换// 直接把构造string参数包往下传,直接用string参数包构造stringlt1.emplace_back("1111111111111111111");cout << "**********************************" << endl;bs::list<pair<bs::string, int>> lt2;// 我们自己写的有一个哨兵位cout << "**********************************" << endl;// 传左值,跟push_back一样,走拷贝构造pair<bs::string, int> kv1("11111111111", 1);lt2.push_back(kv1);lt2.emplace_back(kv1);cout << "**********************************" << endl;// 传右值,跟push_back一样,走移动构造pair<bs::string, int> kv2("11111111111", 1);lt2.push_back(move(kv2));pair<bs::string, int> kv3("11111111111", 1);lt2.emplace_back(move(kv3));cout << "**********************************" << endl;// emplace_back的效率略高一筹// 下面这句代码不能用万能引用,因为花括号初始化列表本身没有具体的类型信息// 模板参数 x 无法被推导出来// 模板参数推导规则:// 1. 模板参数推导需要明确的类型信息// 2. 但{ ... }在编译期没有确定的类型// 3. 编译器不知道 x 应该是什么类型//lt2.push_back({ "1111111111111111111", 1 }); // 多参数构造的隐式类型转换// 把类型给出来就行了,或者不用万能引用,用一个左值和一个右值引用// 编译器底层就是用一个左值和一个右值引用lt2.push_back(std::pair<bs::string, int>("11111111111111111111", 1));// 花括号{},编译器会识别为initializer list,而initializer list的参数类型要相同// 所以编译器报错: “initializer list”: 不是“_Valty”的有效模板参数// lt2.emplace_back({ "1111111111111111111", 1 }); // 不支持// 参数包传下去,最后直接构造容器上的对象lt2.emplace_back("1111111111111111111", 1);cout << "**********************************" << endl;return 0;
}
2. 新的类功能
2.1 默认的移动构造和移动赋值
- 原来C++类中,有6个默认成员函数:构造函数/析构函数/拷⻉构造函数/拷⻉赋值重载/取地址重载/const 取地址重载,最后重要的是前4个,后两个⽤处不⼤,默认成员函数就是我们不写编译器会⽣成⼀个默认的。C++11新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
- 如果你没有⾃⼰实现移动构造函数,且没有实现析构函数、拷⻉构造、拷⻉赋值重载中的任意⼀个。那么编译器会⾃动⽣成⼀个默认移动构造。默认⽣成的移动构造函数,对于内置类型成员会执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调⽤移动构造,没有实现就调⽤拷⻉构造。
- 如果你没有⾃⼰实现移动赋值重载函数,且没有实现析构函数、拷⻉构造、拷⻉赋值重载中的任意⼀个,那么编译器会⾃动⽣成⼀个默认移动赋值。默认⽣成的移动赋值函数,对于内置类型成员会执⾏逐成员按字节移动,⾃定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调⽤移动赋值,没有实现就调⽤拷⻉赋值。(默认移动赋值跟上⾯移动构造完全类似)
- 如果你提供了移动构造或者移动赋值,编译器不会⾃动提供拷⻉构造和拷⻉赋值。
2.2 成员变量声明时给缺省值
成员变量声明时给缺省值是给初始化列表⽤的,如果没有显式在初始化列表初始化,就会在初始化列表⽤这个缺省值初始化
2.3 default和delete
C++11可以让你更好的控制要使⽤的默认函数。假设你要使⽤某个默认的函数,但是因为⼀些原因这个函数没有默认⽣成。⽐如:我们提供了拷⻉构造,就不会⽣成移动构造了,那么我们可以使⽤default关键字显式指定移动构造⽣成。
如果想要限制某些默认函数的⽣成,在C++98中,是该函数设置成private,并且只声明不定义,这样只要其他⼈想要调⽤就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不⽣成对应函数的默认版本,称=delete修饰的函数为删除函数。
2.1 / 2.2 / 2.3
class Person
{
public:Person(const char* name = "", int age = 0): _name(name), _age(age){}// 委托构造Person(int i, const char* name = "", int age = 0):Person(name, age){_i = i;}// 强制不让生成,不期望这个类的对象被拷贝//Person(const Person& p) = delete;//Person(const Person& p)//:_name(p._name)//,_age(p._age)//{}// 强制生成//Person(Person&& p) = default;//Person& operator=(const Person& p)//{// if(this != &p)// {// _name = p._name;// _age = p._age;// }// return *this;//}//~Person()//{}private:bs::string _name;int _age;int _i = 0;
};int main()
{Person s1;Person s2 = s1;Person s3 = move(s1);Person s4;s4 = move(s2);
}
2.4 final和override
我截图了C++多态对final和override的描述,具体可以去C++多态文章里了解。
3. STL中的一些变化
- 下图圈起来的就是STL中的新容器,但是实际最有⽤的是unordered_map和unordered_set。这两个我们前⾯已经进⾏了⾮常详细的讲解,其他的⼤家了解⼀下即可。
- STL中容器的新接⼝也不少,最重要的就是右值引⽤和移动语义相关的push/insert/emplace系列接⼝和移动构造和移动赋值,还有initializer_list版本的构造等,这些前⾯都讲过了,还有⼀些⽆关痛痒的如cbegin/cend等需要时查查⽂档即可。
- 容器的范围for遍历,这个在容器部分也讲过了。