C++11部分内容(中)
注:C++学习之路.cpp: 每天更新创库有着要用的比较长的list与string的简单的底层代码,而且String.h包含了要用的头文件,下面的代码例子我就只包括main()函数
一.引用折叠
1.特点:
<1>.C++中不能直接定义引用的引用如 int& && r = i; ,这样写会直接报错,通过模板或typedef 中的类型操作可以构成引用的引用。
<2>. 通过模板或typedef中的类型操作可以构成引用的引用时,这时C++11给出了⼀个引用折叠的规 则:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。
2.例子
下面的程序中很好的展示了模板和typedef时构成引用的引用时的引用折叠规则,⼤家需要⼀个⼀ 个仔细理解⼀下。
2.1.显示实例化
例1.typedef
int main() { typedef int& lref; typedef int&& rref; int n = 0; lref& r1 = n; // r1 的类型是int& lref&& r2 = n; // r2 的类型是int& rref& r3 = n; // r3 的类型是int& rref&& r4 = 1; // r4 的类型是int&& return 0; }例2.模板
大框架:
// 由于引⽤折叠限定,f1实例化以后总是⼀个左值引⽤ template<class T> void f1(T& x) { int a = 10; T b = a; cout << &a << endl; cout << &b << endl; } // 由于引⽤折叠限定,f2实例化后可以是左值引⽤,也可以是右值引⽤ template<class T> void f2(T&& x) { int a = 10; T b = a; cout << &a << endl; cout << &b << endl; } int main() { int n = 10; ... return 0; }main函数内:
//没有折叠->实例化为void f1(int& x),T为int f1<int>(n); //f1<int>(0); 报错,0为右值,无法传参输出结果:
//折叠引用->实例化为void f1(int& x),T为int& f1<int&>(n); //f1<int&>(0); 报错,0为右值,无法传参输出结果:
//折叠->实例化为void f1(const int& x),T为const int& f1<const int&>(n); f1<const int&>(0); //const引用可以正常引用右值输出结果:
// 折叠->实例化为void f1(const int& x),T为const int&& f1<const int&&>(n); f1<const int&&>(0); //const引用可以正常引用右值注:此时要将T b = a;和cout << &b << endl;注释掉
输出结果:
//没有折叠->实例化为void f2(int&& x),T为int //f2<int>(n); // 报错,右值引用不可以引用左值,除非move(左值) f2<int>(0);输出结果:
//折叠->实例化为void f2(int& x),T为int& f2<int&>(n); //f2<int&>(0); // 报错,0为右值,无法传参输出结果:
// 折叠->实例化为void f2(int&& x),T为int&& //f2<int&&>(n); // 报错 f2<int&&>(0);注:此时要将T b = a;和cout << &b << endl;注释掉
输出结果:
2.2.隐式实例化
大框架:
template<class T> void Function(T&& t) { int a = 0; T x = a; //x++; cout <<"a的地址:" << &a << endl; cout <<"x的地址:" <<&x << endl << endl; } int main() { ... return 0; }main函数内:
int main() { // 10是右值,推导出T为int,模板实例化为void Function(int&& t); Function(10); int a; // a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t) Function(a); // a是右值,但是std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t) Function(std::move(a)); const int b = 8; // b是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int&t) // 所以Function内部会编译报错,x不能++ Function(b);// const 左值 // std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&t) // 所以Function内部会编译报错,x不能++ Function(std::move(b)); //const 右值 return 0; }输出结果:
3.完美转发
特点:
<1>.Function(T&&t)函数模板程序中,传左值实例化以后是左值引用的Function函数,传右值实例化以后是右值引用的Function函数。
<2>. 结合我们之前的讲解,变量表达式都是左值属性,也就意味着⼀个右值被右值引用绑定 后,右值引用变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传递给下⼀层函数Fun,那么匹配的都是左值引用版本的Fun函数。
例如:
void Fun(int& x) {cout << "左值引用" << endl; } void Fun(const int& x) {cout << "const 左值引用" << endl; } void Fun(int&& x) {cout << "右值引用" << endl; } void Fun(const int&& x) {cout << "const 右值引用" << endl; } template<class T> void Function(T && t){Fun(t);} int main() { // 10是右值,推导出T为int,模板实例化为void Function(int&& t) Function(10); int a; // a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t) Function(a); // std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t) Function(std::move(a));// 右值 const int b = 8; // b是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int& t) Function(b); // const 左值 // std::move(b)右值,推导出T,const int,模板实例化为void Function(const int&&t) Function(std::move(b)); // const 右值 return 0; }输出结果:
这里我们想要保持t对象的属性, 就需要使用完美转发实现。
完美转发forward本质是⼀个函数模板,他主要还是通过引用折叠的方式实现,下面示例中传递给 Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引用返回;传递给 Function的实参是左值,T被推导为int&,引用折叠为左值引用,forward内部t被强转为左值引用返回。
那么我们将Fun(t);改为Fun(forward<T>(t));那么就会发生变化。
输出结果:
4.完美转发对string的改变
有了右值引用和引用折叠后,我们可以将原来List的底层改变一下,例如将insert改变为:
iterator insert(iterator pos, const T& x) { Node* cur = pos._node; Node* prev = cur->_prev; Node* newnode = new Node(x); // prev newnode cur newnode->_next = cur; cur->_prev = newnode; newnode->_prev = prev; prev->_next = newnode; ++_size; return newnode; } iterator insert(iterator pos, T&& x) { Node* cur = pos._node; Node* prev = cur->_prev; Node* newnode = new Node(forward<T>(x)); // prev newnode cur newnode->_next = cur; cur->_prev = newnode; newnode->_prev = prev; prev->_next = newnode; ++_size; return newnode; }这样以后,当传kong::string类左值时就会调用拷贝构造,当传kong::string类右值时就会调用移动构造,这样就可以提高效率,但是这样就显得十分的繁琐,但是完美转发可以解决这个问题。
将其改为:
template<class X> iterator insert(iterator pos, X&& x) { //cout << "segwgsegw" << endl; Node* cur = pos._node; Node* prev = cur->_prev; Node* newnode = new Node(forward<X>(x)); //cout << "qwdqdqa" << endl; // prev newnode cur newnode->_next = cur; cur->_prev = newnode; newnode->_prev = prev; prev->_next = newnode; ++_size; return newnode; }当然其他的也要改变,完整的代码就在无忧.芙桃/C++学习之路.cpp - 码云 - 开源中国这里面。
二.可变参数模板
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) {}<2>.我们用...(省略号)来指出⼀个模板参数或函数参数的表示⼀个包,在模板参数列表中,class.../typename...指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟...指出 接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板 ⼀样,每个参数实例化时遵循引用折叠规则。
<3>.可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数
<4>.这里我们可以使用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; }原理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)
2.包扩展
特点:
<1>.对于⼀个参数包,我们除了能计算他的参数个数,我们还可以做的事就是扩展它,当扩展⼀个 包时,我们还要提供用于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(...)来触发扩展操作。
例如:
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(); Print(1); Print(1, string("xxxxx")); Print(1, string("xxxxx"), 2.2); return 0; }输出结果:
底层的实现细节如图所示:
本质编译器将可变参数模板通过模式的包扩展,编译器推导的以下几个重载函数函数
void ShowList() { cout << endl; } void ShowList(double z) { cout << z << " "; ShowList(); } void ShowList(string y, double z) { cout << y << " "; 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); }<2>. C++还支持更复杂的包扩展,直接将参数包依次展开依次作为实参给⼀个函数去处理。
3.empalce系列接口
特点:
<1>.C++11以后STL容器新增了empalce系列的接口,empalce系列的接口均为模板可变参数,功能上兼容push和insert系列,但是empalce还支持新玩法,假设容器为container,empalce还支持直接插入构造T对象的参数,这样有些场景会更高效⼀些,可以直接在容器空间上构造T对象。
例如使用库里的list的emplace_back:
int main() { list<bit::string> lt; bit::string s1("111111111111"); bit::string s2("111111111111"); // 传左值,跟push_back一样,走拷贝构造 lt.emplace_back(s1); cout << "*********************************" << endl; lt.push_back(s1); cout << "*********************************" << endl; // 右值,跟push_back一样,走移动构造 lt.emplace_back(move(s1)); cout << "*********************************" << endl; lt.push_back(move(s2)); cout << "*********************************" << endl; lt.emplace_back("111111111111"); cout << "*********************************" << endl; // 直接传参,隐式类型转换 lt.push_back("111111111111"); cout << "*********************************" << endl; return 0; }输出结果:
问:为什么emplace_back一会走拷贝构造,一会走构造,一会又走移动构造?
解释:s1是一个左值,类型是kong::string,编译器看到左值,触发引用折叠规则,把Args&& 推导为左值引用,而底层节点构造,代码变成了new Node(kong:string&),kong::string的构造函数中,能接收s1的左值引用的,只有拷贝构造函数string(const string&);走移动构造的过程也相似;传入"111111111111"时,b编译器会认为这是一个字符串常量,类型是const char*,类型推导时,编译器把 Args&&推导为const char*(严格来说是常数组引用,这里简化为指针),底层节点构造时,代码变成了new Node(const char*),所以它它匹配到了普通带参构函数。
如果使用我们自己的emplace_back函数的话,
int main() { bit::list<pair<bit::string, int>> lt1; cout << "*********************************" << endl; // 跟push_back一样 // 构造pair + 拷贝/移动构造pair到list的节点中data上 pair<bit::string, int> kv("苹果", 1); cout << "*********************************" << endl; lt1.emplace_back(kv); cout << "*********************************" << endl; // 21:15 // 跟push_back一样 lt1.emplace_back(move(kv)); cout << "*********************************" << endl; // 这里达到的效果是push_back做不到的 //lt1.emplace_back({ "苹果", 1 }); // 不支持 lt1.emplace_back("苹果", 1 ); // cout << "*********************************" << endl; lt1.push_back({ "苹果", 1 }); //里面的万能引用无法识别{ "苹果", 1 }的类型为pair,要使用原来的push_back cout << "*********************************" << endl; return 0; }输出结果:
<2>. emplace_back总体而言是更高效,推荐以后使用emplace系列替代insert和push系列
注意:传递参数包过程中,如果是 Args&&... args 的参数包,要用完美转发参数包,方式如下 std::forward(args)... ,否则编译时包扩展后右值引用变量表达式就变成了左值。
