【C++】23. C++11(上)
文章目录
- 一、C++11的发展历史
- 二、列表初始化
- 1、C++98传统的{}
- 2、C++11中的{}
- 3、C++11中的std::initializer_list
- 三、右值引⽤和移动语义
- 1、左值和右值
- 2、左值引⽤和右值引⽤
- 3、引⽤延⻓⽣命周期
- 4、左值和右值的参数匹配
- 5、右值引⽤和移动语义的使⽤场景
- 1)左值引⽤主要使⽤场景回顾
- 2)移动构造和移动赋值
- 3)右值引⽤和移动语义解决传值返回问题
- 4)右值引⽤和移动语义在传参中的提效
- 6、类型分类
- 7、引⽤折叠
- 8、完美转发
一、C++11的发展历史
C++11 是 C++ 的第⼆个主要版本,并且是从 C++98 起的最重要更新。它引⼊了⼤量更改,标准化了既有实践,并改进了对 C++ 程序员可⽤的抽象。在它最终由 ISO 在 2011 年 8 ⽉ 12 ⽇采纳前,⼈们曾使⽤名称“C++0x”,因为它曾被期待在2010年之前发布。C++03 与 C++11 期间花了 8 年时间,故⽽这是迄今为⽌最⻓的版本间隔。从那时起,C++ 有规律地每3年更新一次。
二、列表初始化
1、C++98传统的{}
C++98中⼀般数组和结构体可以⽤{}进⾏初始化。
struct Point
{int _x;int _y;
};int main()
{//C++98支持int a1[] = { 1,2,3,4,5 };int a2[5] = { 0 };Point p1 = { 1,2 };return 0;
}
2、C++11中的{}
- C++11以后想统⼀初始化⽅式,试图实现⼀切对象皆可⽤{}初始化,{}初始化也叫做列表初始化。
- 内置类型⽀持,⾃定义类型也⽀持,⾃定义类型本质是类型转换,中间会产⽣临时对象,最后优化了以后变成直接构造。
- {}初始化的过程中,可以省略掉=。
- C++11列表初始化的本意是想实现⼀个⼤统⼀的初始化⽅式,其次他在有些场景下带来很多便利,如容器push/inset多参数构造的对象时,{}初始化会很⽅便。
#include<vector>
#include<iostream>
using namespace std;struct Point
{int _x;int _y;
};class Date
{
public:Date(int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day){}Date(const Date& d):_year(d._year), _month(d._month), _day(d._day){}
private:int _year;int _month;int _day;
};int main()
{//C++98支持:int a1[] = { 1,2,3,4,5 };int a2[5] = { 0 };Point p1 = { 1,2 };//C++11支持://内置类型int x1 = { 2 };//自定义类型//本质是用{ 2025,2,1 }先构造一个临时对象,//临时对象再去拷贝构造d1,编译器优化为{ 2025,2,1 }直接构造初始化d1Date d1 = { 2025,2,1 };//d2引用的是{ 2025,2,1 }构造的临时对象const Date& d2 = { 2025,2,1 };//C++98支持单参数类型转换,不用{}Date d3 = { 2025 };//C++11Date d4 = 2025;//C++98//C++11支持省略=Point p2{ 1,2 };int x2{ 2 };Date d5{ 2025,2,1 };const Date& d6{ 2025,2,1 };//比起有名和匿名对象,这里直接使用{}更方便vector<Date> v;v.push_back(d1);v.push_back(Date{ 2025,2,1 });v.push_back({ 2025,2,1 });return 0;
}
3、C++11中的std::initializer_list
- 上⾯的初始化已经很⽅便,但是对象容器初始化还是不太⽅便,⽐如⼀个vector对象,我想⽤N个值去构造初始化,那么我们得实现很多个构造函数才能⽀持,
vector<int> v1 = {1,2,3};vector<int> v2 = {1,2,3,4,5};
- C++11库中提出了⼀个std::initializer_list的类,
std::initializer_list il = { 10, 20, 30 };
,这个类的本质是底层开⼀个数组,将数据拷⻉过来,std::initializer_list内部有两个指针分别指向数组的开始和结束。 - 这是他的⽂档:initializer_list,std::initializer_list⽀持迭代器遍历。
- 容器⽀持⼀个std::initializer_list的构造函数,也就⽀持任意多个值构成的
{x1,x2,x3...}
进⾏初始化。STL中的容器⽀持任意多个值构成的{x1,x2,x3...}
进⾏初始化,就是通过std::initializer_list的构造函数⽀持的。
//STL中的容器都增加了⼀个initializer_list的构造
vector(initializer_list<value_type> il,
const allocator_type& alloc = allocator_type());list(initializer_list<value_type> il,
const allocator_type& alloc = allocator_type());map(initializer_list<value_type> il,
const key_compare& comp =key_compare(),
const allocator_type& alloc = allocator_type());
// ...
template<class T>
class vector {
public:typedef T* iterator;vector(initializer_list<T> l){for (auto e : l)push_back(e)}
private:iterator _start = nullptr;iterator _finish = nullptr;iterator _endofstorage = nullptr;
};// 另外,容器的赋值也支持initializer_list的版本
vector& operator= (initializer_list<value_type> il);
map& operator= (initializer_list<value_type> il);
#include<iostream>
#include<vector>
#include<map>
using namespace std;int main()
{std::initializer_list<int> mylist = { 10,20,30 };//initializer_list对象中存放两个指针begin和endcout << sizeof(mylist) << endl;//x86下运行结果:8//这两个指针的值跟i的地址很接近,说明开辟的数组存放在栈上int i = 0;cout << &i << endl;cout << mylist.begin() << endl;cout << mylist.end() << endl;//使用{}列表时语义的区别vector<int> v1({ 1,2,3,4,5 });//直接构造vector<int> v2 = { 1,2,3,4,5 };//构造临时对象+临时对象拷贝v2 -> 编译器优化为直接构造const vector<int>& v3 = { 1,2,3,4,5 };//引用临时对象//initailizer_list + {}pair对象的隐式类型转换map<string, string> dict = { {"sort","排序"},{"isnert","插入"} };//initailizer_list支持赋值v1 = { 10,20,30,40,50 };return 0;
}
三、右值引⽤和移动语义
C++98的C++语法中就有引⽤的语法,⽽C++11中新增了的右值引⽤语法特性,C++11之后我们之前学习的引⽤就叫做左值引⽤。⽆论左值引⽤还是右值引⽤,都是给对象取别名。
1、左值和右值
- 左值是⼀个表⽰数据的表达式(如变量名或解引⽤的指针),⼀般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。
- 右值也是⼀个表⽰数据的表达式,要么是字⾯值常量、要么是表达式求值过程中创建的临时对象等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
- 值得⼀提的是,左值的英⽂简写为lvalue,右值的英⽂简写为rvalue。传统认为它们分别是left value、right value的缩写。现代C++中,lvalue被解释为loactor value的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象。⽽rvalue被解释为read value,指的是那些可以提供数据值,但是不可以寻址,例如:临时变量,字⾯量常量,存储于寄存器中的变量等,也就是说左值和右值的核⼼区别就是能否取地址。
#include<iostream>
using namespace std;int main()
{//左值:可以取地址,下面的p,b,c,*p,s,s[0]都是左值int* p = new int(0);int b = 1;const int c = b;*p = 10;string s("11111111");s[0] = 'x';cout << &c << endl;cout << (void*)&s[0] << endl;//左值引用: 给左值取别名int& r1 = b;int*& r2 = p;int& r3 = *p;string& r4 = s;char& r5 = s[0];//右值:不能取地址。下面的10、x+y、fmin(x+y)、string("11111")都是右值double x = 1.1, y = 2.2;10;x + y;fmin(x, y);string("11111");//右值引用: 给右值取别名int&& rr1 = 10;double&& rr2 = x + y;double&& rr3 = fmin(x, y);string&& rr4 = string("11111");return 0;
}
2、左值引⽤和右值引⽤
-
Type& r1 = x; Type&& rr1 = y;
第⼀个语句就是左值引⽤,左值引⽤就是给左值取别名,第⼆个就是右值引⽤,同理,右值引⽤就是给右值取别名。 -
左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值。
-
右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)。
-
move是库⾥⾯的⼀个函数模板,本质内部是进⾏强制类型转换。
template <class _Ty>
remove_reference_t<_Ty>&& move(_Ty&& _Arg)
{return static_cast<remove_reference_t<_Ty>&&>(_Arg);
}
- 需要注意的是变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量的属性是左值。
- 语法层⾯看,左值引⽤和右值引⽤都是取别名,不开空间。从汇编底层的⻆度看下⾯代码中r1和rr1汇编层实现,底层都是⽤指针实现的,没什么区别。
#include<iostream>
using namespace std;
int main()
{//左值int* p = new int(0);int b = 1;const int c = b;*p = 10;string s("11111111");s[0] = 'x';//左值引用int& r1 = b;int*& r2 = p;int& r3 = *p;string& r4 = s;char& r5 = s[0];//右值double x = 1.1, y = 2.2;10;x + y;fmin(x, y);string("11111");//右值引用int&& rr1 = 10;double&& rr2 = x + y;double&& rr3 = fmin(x, y);string&& rr4 = string("11111");//左值引用不能直接引用右值,但是const左值引用可以引用右值const int& rx1 = 10;const double& rx2 = x + y;const double& rx3 = fmin(x, y);const string& rx4 = string("11111");//右值引用不能直接引用左值,但是右值引用可以引用move(左值)int&& rrx1 = move(b);int*&& rrx2 = move(p);int&& rrx3 = move(*p);string&& rrx4 = move(s);//(string&&)s//左值引用、右值引用的属性都是左值cout << &r1 << endl;cout << &rr1 << endl;//rr1的属性是左值,不能直接被右值引用绑定,move后即可int&& rrx6 = move(rr1);return 0;
}
3、引⽤延⻓⽣命周期
右值引⽤可⽤于为临时对象延⻓⽣命周期,右值引用可以修改;const的左值引⽤也能延⻓临时对象⽣存期,但这些对象⽆法被修改。
#include<iostream>
#include<string>
using namespace std;int main()
{string s1 = "Test ";string&& r2 = s1 + s1;//右值引用也可以延长生命周期r2 += "yyy";//右值引用可以修改const string& r1 = s1 + s1;//const左值引用可以延长生命周期//r1 += "xxx";//err: const左值引用不能修改return 0;
}
4、左值和右值的参数匹配
- C++98中,我们实现⼀个const左值引⽤作为参数的函数,那么实参传递左值和右值都可以匹配。
- C++11以后,分别重载左值引⽤、const左值引⽤、右值引⽤作为形参的f函数,那么实参是左值会匹配f(左值引⽤),实参是const左值会匹配f(const 左值引⽤),实参是右值会匹配f(右值引⽤)。
- 右值引⽤变量在⽤于表达式时属性是左值。
#include<iostream>
using namespace std;void f(int& x)
{cout << "左值引用重载 f(" << x << ")" << endl;
}void f(const int& x)
{cout << "const左值引用重载 f(" << x << ")" << endl;
}void f(int&& x)
{cout << "右值引用重载 f(" << x << ")" << endl;
}int main()
{int i = 1;const int ci = 2;f(i);//调用f(int& x)f(ci);//调用f(const int& x)f(3);//调用f(int&& x),如果没有f(int&&)重载,会调用f(const int&)int&& x = 1;f(x);//右值引用本身的属性是左值f(move(x));//左值move后变成右值,调用f(int&& x)return 0;
}
运行结果:
5、右值引⽤和移动语义的使⽤场景
1)左值引⽤主要使⽤场景回顾
左值引⽤主要使⽤场景是在函数中左值引⽤传参和左值引⽤传返回值时减少拷⻉,同时还可以修改实参和修改返回对象。左值引⽤已经解决⼤多数场景的拷⻉效率问题,但是有些场景不能使⽤传左值引⽤返回,如addStrings和generate函数,C++98中的解决⽅案只能是被迫使⽤输出型参数解决。那么C++11以后这⾥可以使⽤右值引⽤做返回值解决吗?显然是不可能的,因为这⾥的本质是返回对象是⼀个局部对象,函数结束这个对象就析构销毁了,右值引⽤返回也⽆法改变对象已经析构销毁的事实。
class Solution {
public://传值返回需要拷贝string addStrings(string num1, string num2) {string str;int end1 = num1.size() - 1, end2 = num2.size() - 1;//进位 int next = 0;while (end1 >= 0 || end2 >= 0){int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;int ret = val1 + val2 + next;next = ret / 10;ret = ret % 10;str += ('0' + ret);}if (next == 1){str += '1';}reverse(str.begin(), str.end());return str;}
};class Solution {
public://这里的传值返回拷贝代价就太大了 vector<vector<int>> generate(int numRows) {vector<vector<int>> vv(numRows);for (int i = 0; i < numRows; ++i){vv[i].resize(i + 1, 1);}for (int i = 2; i < numRows; ++i){for (int j = 1; j < i; ++j){vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];}}return vv;}
};
addStrings函数的解析:
2)移动构造和移动赋值
-
移动构造函数是⼀种构造函数,类似拷⻉构造函数,移动构造函数要求第⼀个参数是该类类型的右值引⽤,如果还有其他参数,额外的参数必须有缺省值。
-
移动赋值是⼀个赋值运算符的重载,他跟拷⻉赋值构成函数重载,类似拷⻉赋值函数,移动赋值函数要求第⼀个参数是该类类型的右值引⽤。
-
对于像string/vector这样的深拷⻉的类或者包含深拷⻉的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第⼀个参数都是右值引⽤的类型,他的本质是要“窃取”引⽤的右值对象的资源,⽽不是像拷⻉构造和拷⻉赋值那样去拷⻉资源,因此可以提⾼效率。
下⾯的zsy::string样例实现了移动构造和移动赋值,结合场景来理解。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<assert.h>
using namespace std;namespace zsy
{class string{public:typedef char* iterator;typedef const char* const_iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}const_iterator begin() const{return _str;}const_iterator end() const{return _str + _size;}string(const char* str = ""):_size(strlen(str)), _capacity(_size){cout << "string(char* str)-构造" << endl;_str = new char[_capacity + 1];strcpy(_str, str);}void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}//拷贝构造string(const string& s){cout << "string(const string& s)-拷贝构造" << endl;reserve(s._capacity);for (auto ch : s){push_back(ch);}}//移动构造string(string&& s){cout << "string(string&& s)-移动构造" << endl;//转移掠夺你的资源swap(s);}//拷贝赋值string& operator=(const string& s){cout << "string& operator=(const string& s)-拷贝赋值" << endl;if (this != &s){_str[0] = '\0';_size = 0;reserve(s._capacity);for (auto ch : s){push_back(ch);}}return *this;}//移动赋值string& operator=(string&& s){cout << "string& operator=(string&& s)-移动赋值" << endl;swap(s);return *this;}~string(){delete[] _str;_str = nullptr;}char& operator[](size_t pos){assert(pos < _size);return _str[pos];}void reserve(size_t n){if (n > _capacity){char* tmp = new(char[n + 1]);if (_str){strcpy(tmp, _str);delete[] _str;}_str = tmp;_capacity = n;}}void push_back(char ch){//扩容if (_size >= _capacity){size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}_str[_size] = ch;++_size;_str[_size] = '\0';}string& operator+=(char ch){push_back(ch);return *this;}const char* c_str() const{return _str;}size_t size() const{return _size;}private:char* _str = new char('\0');size_t _size = 0;size_t _capacity = 0;};
}int main()
{zsy::string s1("11111");//构造zsy::string s2 = s1;//拷贝构造zsy::string s3 = zsy::string("22222");//构造+移动构造->直接构造zsy::string s4 = move(s1);//移动构造return 0;
}
运行结果:
3)右值引⽤和移动语义解决传值返回问题
namespace zsy
{string addStrings(string num1, string num2){string str;int end1 = num1.size() - 1, end2 = num2.size() - 1;//进位int next = 0;while (end1 >= 0 || end2 >= 0){int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;int ret = val1 + val2 + next;next = ret / 10;ret = ret % 10;str += ('0' + ret);}if (next == 1){str += '1';}reverse(str.begin(), str.end());return str;}
};// 场景1
int main()
{//拷贝构造zsy::string ret = zsy::addStrings("11111", "22222");//右值cout << ret.c_str() << endl;return 0;
}// 场景2
int main()
{//拷贝赋值zsy::string ret;ret = zsy::addStrings("11111", "22222");//右值cout << ret.c_str() << endl;return 0;
}
右值对象构造,只有拷⻉构造,没有移动构造的场景:
- 下图展⽰了vs2019 debug环境下编译器对拷⻉的优化,左边为不优化的情况下,进行两次拷⻉构造。中间为编译器的第一代优化,将连续步骤中的拷⻉构造合⼆为⼀变为⼀次拷⻉构造。
- 需要注意的是在vs2019的release和vs2022的debug和release会进行第二代优化。str对象的构造,str拷⻉构造临时对象,临时对象拷⻉构造ret对象,合三为⼀,变为直接构造。
- linux下可以将下⾯代码拷⻉到test.cpp⽂件,编译时⽤
g++ test.cpp -fno-elideconstructors
的⽅式关闭构造优化。
我们在vs2022的debug版本下运行:
发现已经优化掉了两次拷贝构造,从而变为直接构造。
右值对象构造,有拷⻉构造,也有移动构造的场景:
-
下图展⽰了vs2019 debug环境下编译器对移动的优化,左边为不优化的情况下,进行两次移动构造。中间为编译器的第一代优化,将连续步骤中移动构造合⼆为⼀变为⼀次移动构造。
-
需要注意的是在vs2019的release和vs2022的debug和release会进行第二代优化。str对象的构造,str移动构造临时对象,临时对象移动构造ret对象,合三为⼀,变为直接构造。
-
linux下可以将下⾯代码拷⻉到test.cpp⽂件,编译时⽤
g++ test.cpp -fno-elideconstructors
的⽅式关闭构造优化。
我们在vs2022的debug版本下运行:
发现已经优化掉了两次移动构造,从而变为直接构造。
右值对象赋值,只有拷⻉构造和拷⻉赋值,没有移动构造和移动赋值的场景:
- 下图左边展⽰了vs2019 debug和
g++ test.cpp -fno-elide-constructors
关闭优化环境下编译器的处理,⼀次拷⻉构造,⼀次拷⻉赋值。 - 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码会进⼀步优化,优化掉拷贝构造,直接构造要返回的临时对象。str本质是临时对象的引⽤,底层⻆度⽤指针实现。
我们在vs2022的debug版本下运行:
可以看到优化掉了一次拷贝构造。
右值对象赋值,既有拷⻉构造和拷⻉赋值,也有移动构造和移动赋值的场景:
- 下图左边展⽰了vs2019 debug和
g++ test.cpp -fno-elide-constructors
关闭优化环境下编译器的处理,⼀次移动构造,⼀次移动赋值。 - 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码会进⼀步优化,优化掉拷贝构造,直接构造要返回的临时对象。str本质是临时对象的引⽤,底层⻆度⽤指针实现。
我们在vs2022的debug版本下运行:
可以看到优化掉了一次拷贝构造。
总结:
- 深拷贝的自定义类型:如vector/string/map…,实现移动构造和移动赋值有很大的价值,可以减少拷贝,提高效率。
- 浅拷贝的自定义类型,如Date/pair<int,int>…,实现移动构造和移动赋值没有很大的价值,本身拷贝是很快的,对效率影响不大。
4)右值引⽤和移动语义在传参中的提效
- 查看STL⽂档我们发现C++11以后容器的push和insert系列的接⼝都增加右值引⽤的版本。
void push_back (const value_type& val);void push_back (value_type&& val);iterator insert (const_iterator position, value_type&& val);iterator insert (const_iterator position, const value_type&& val);
-
当实参是⼀个左值时,容器内部继续调⽤拷⻉构造进⾏拷⻉,将对象拷⻉到容器空间中的对象。
-
当实参是⼀个右值,容器内部则调⽤移动构造,将右值对象的资源移动到容器空间的对象上。
-
把我们之前模拟实现的zsy::list拷⻉过来,⽀持右值引⽤参数版本的push_back和insert。
List.h:
namespace zsy
{template<class T>struct list_node{T _data;list_node<T>* _next;list_node<T>* _prev;list_node() = default;template<class X>list_node(X&& data):_data(forward<X>(data)), _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){}Self& operator++(){_node = _node->_next;return *this;}Ref operator*(){return _node->_data;}bool operator!=(const Self& it){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 _head->_next;}iterator end(){return _head;}void empty_init(){//空初始化(只有哨兵位)_head = new Node;_head->_next = _head;_head->_prev = _head;_size = 0;}list(){empty_init();}//构造: 初始化列表list(initializer_list<T> il){empty_init();for (auto& e : il){push_back(e);}}void push_back(const T& x){insert(end(), x);}//右值引用版本template<class X>void push_back(X&& x){insert(end(), forward<X>(x));}iterator insert(iterator pos, const T& x){Node* newnode = new Node(x);Node* cur = pos._node;Node* prev = cur->_prev;newnode->_next = cur;cur->_prev = newnode;newnode->_prev = prev;prev->_next = newnode;++_size;return iterator(newnode);}//右值引用版本template<class X>iterator insert(iterator pos, X&& x){Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(forward<X>(x));newnode->_next = cur;cur->_prev = newnode;newnode->_prev = prev;prev->_next = newnode;++_size;return iterator(newnode);}private:Node* _head;//哨兵位size_t _size;};
}
Test.cpp:
#include"List.h"int main()
{zsy::list<zsy::string> lt;zsy::string s1("11111");lt.push_back(s1);//传左值,拷贝构造cout << endl;zsy::string s2("22222");lt.push_back(move(s2));//传右值,移动构造cout << endl;lt.push_back("33333");//构造return 0;
}
运行结果:
6、类型分类
-
C++11以后,进⼀步对类型进⾏了划分,右值被划分纯右值(pure value,简称prvalue)和将亡值(expiring value,简称xvalue)。
-
纯右值是指那些字⾯值常量或者求值结果相当于字⾯值,又或是⼀个不具名的临时对象。如:
42、true、nullptr
或str.substr(1, 2)、str1 + str2
等传值返回函数调⽤,或整形a、b,a++,a+b
等。纯右值和将亡值是C++11才提出的,C++11中的纯右值概念划分等价于C++98中的右值。 -
将亡值是指返回右值引⽤的函数的调⽤表达式和转换为右值引⽤的转换函数的调⽤表达,如
move(x)、static_cast<X&&>(x)
。 -
泛左值(generalized value,简称glvalue),泛左值包含将亡值和左值。
-
值类别-cppreference.com 和 Value categories这两个是关于值类型的中⽂和英⽂的官⽅⽂档。
7、引⽤折叠
-
C++中不能直接定义引⽤的引⽤如
int& && r = i;
,这样写会直接报错,只有通过模板或 typedef 中的类型操作才可以构成引⽤的引⽤。 -
通过模板或 typedef 中的类型操作可以构成引⽤的引⽤时,这时C++11给出了⼀个引⽤折叠的规则:右值引⽤的右值引⽤折叠成右值引⽤,所有其他组合均折叠成左值引⽤。
-
像f2这样的函数模板中,
T&& x
参数看起来是右值引⽤参数,但是由于引⽤折叠的规则,他传递左值时就是左值引⽤,传递右值时就是右值引⽤,这种函数模板的参数也叫做万能引⽤。 -
Function(T&& t)
函数模板程序中,假设实参是int右值,模板参数T的推导int,实参是int左值,模板参数T的推导int&,再结合引⽤折叠规则,就实现了实参是左值,实例化出左值引⽤版本形参的Function,实参是右值,实例化出右值引⽤版本形参的Function。
#include<iostream>
using namespace std;//引用折叠限定,f1实例化后总是一个左值引用
template<class T>
void f1(T& x)
{}//引用折叠限定,f2实例化后可以是左值引用,也可以是右值引用
template<class T>
void f2(T&& x)
{}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&&//注:n是左值。0是右值,只能被const左值引用或者右值引用。//没有折叠->实例化为void f1(int& x)f1<int>(n);f1<int>(0); //报错//折叠->实例化为void f1(int& x)f1<int&>(n);f1<int&>(0); //报错//折叠->实例化为void f1(int& x)f1<int&&>(n);f1<int&&>(0); //报错//折叠->实例化为void f1(const int& x)f1<const int&>(n);f1<const int&>(0);//折叠->实例化为void f1(const int& x)f1<const int&&>(n);f1<const int&&>(0);//没有折叠->实例化为void f2(int&& x)f2<int>(n); //报错f2<int>(0);//折叠->实例化为void f2(int& x)f2<int&>(n);f2<int&>(0);//报错//折叠->实例化为void f2(int&& x)f2<int&&>(n); //报错f2<int&&>(0);return 0;
}
#include<iostream>
using namespace std;template<class T>
void Function(T&& t)
{int a = 0;T x = a;//x++;cout << &a << endl;cout << &x << endl << endl;
}int main()
{//10是右值,推导出T为int,模版实例化为void Function(int&& t)Function(10);//b是左值,推导出T为int&,引用折叠,模版实例化为void Function(int& t)int b = 1;Function(b);//move(b)是右值,推导出T为int,模版实例化为void Function(int&& t)Function(move(b));//c是左值,推导出T为const int&,引用折叠,模版实例化为void Function(const int& t)//x不能++,导致Funcion内部会编译报错const int c = 8;// const 左值Function(c);//move(c)是右值,推导出T为const int,模版实例化为void Function(const int&& t)//x不能++,会导致Funcion内部会编译报错Function(move(c));// const 右值return 0;
}
运行结果:
8、完美转发
- Function(T&&t)函数模板程序中,传左值实例化以后是左值引⽤的Function函数,传右值实例化以后是右值引⽤的Function函数。
- 前面提到过,变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量表达式的属性是左值,也就是说Function函数中 t 的属性是左值,那么我们把 t 传递给下⼀层函数Fun,那么匹配的都是左值引⽤版本的Fun函数。这⾥我们想要保持 t 对象的属性,就需要使⽤完美转发实现。
template <class T> T&& forward(typename remove_reference<T>::type& arg);
template <class T> T&& forward(typename remove_reference<T>::type&& arg);
- 完美转发forward本质是⼀个函数模板,他主要还是通过引⽤折叠的⽅式实现,下⾯⽰例中传递给Function的实参是右值,T被推导为int,没有折叠,forward内部 t 被强转为右值引⽤返回。传递给Function的实参是左值,T被推导为int&,引⽤折叠为左值引⽤,forward内部 t 被强转为左值引⽤返回。
#include<iostream>
using namespace std;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)
{//完美转发: 保持对象t的属性Fun(forward<T>(t));
}int main()
{Function(10);//右值int a;Function(a);//左值Function(move(a));//右值const int b = 1;Function(b);//const 左值Function(move(b));//const 右值return 0;
}
运行结果: