六、右值引用和移动语义
1.左值引用和右值引用
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以之前的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
什么是左值?什么是左值引用?
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址,一般可以对它赋值,左值可以出现赋值符号的左边,也可以出现在赋值符号的右边,而右值不能出现在赋值符号左边,只能出现在赋值符号的右边。所以左边的一定是左值,右边的不一定是右值
左值一般可以对它进行赋值,有几个例外就是定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。还有字符串常量、字符串上一个元素都是左值
下面的是一些常见的左值
int main() { // 以下的p、b、c、*p都是左值 int* p = new int(0); int b = 1; const int c = 2; // 以下几个是对上面左值的左值引用 int*& rp = p; int& rb = b; const int& rc = c; int& pvalue = *p; return 0; }
什么是右值,什么是右值引用
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。
下面的右值引用我们暂时不关心
int main() { double x = 1.1, y = 2.2; // 以下几个都是常见的右值 10; x + y; fmin(x, y); // 以下几个都是对右值的右值引用 int&& rr1 = 10; double&& rr2 = x + y; double&& rr3 = fmin(x, y); // 这里编译会报错:error C2106: “=”: 左操作数必须为左值 //10 = 1; //x + y = 1; //fmin(x, y) = 1; return 0; }
但是对于一个字符串,比如下面的,它是一个左值,因为它可以取地址,返回首元素的地址
"xxxxxx"; const char* p = "xxxxxx";
而且这个字符串上的字符也是左值
总之:
左值一定可以取地址,右值无法取出地址
左值引用和右值引用
左值引用就是给左值取别名
右值引用就是给右值取别名
如下所示,右值引用就是使用两个&&即可
int main() { int a = 0; int& r = a; //右值引用 int&& rr = 0; double x = 1.1; double y = 2.2; double&& ret = x + y; return 0; }
其实除了左值引用给左值取别名,右值引用给右值取别名以外。
还可以左值引用给右值取别名,只需要加上一个const就可以了,但是绝不可以直接使用左值引用去引用右值,这会直接报错的
int main() { const int& r = 0; return 0; }
那么能否右值引用给左值取别名呢?首先可以确定的是不可以直接使用,但是可以给左值加上move就可以了,但是move可能会对这个左值造成一些其他影响
int main() { //左值引用给右值取别名 const int& r = 0; //右值引用给左值取别名 int a = 0; int&& r1 = move(a); return 0; }
总结
- 左值和右值的区别就是能否取地址,左值可以取地址,右值不可以取地址
- 左值引用可以给左值取别名,不可以引用右值;右值引用可以给右值取别名,但是不可以引用左值
- const左值引用可以给右值取别名,右值引用可以给move以后的左值取别名
2.右值引用的使用场景和意义
我们先来看左值引用的使用场景和价值
左值引用的使用场景和价值
使用场景: 1、做参数(输出型参数) 2、做返回值
价值:减少拷贝
然后左值引用有一种场景还没有解决:那就是返回局部对象不可以使用左值引用。即下面的场景,我们只能去使用传值返回,不能传左值引用返回,因为无论是左值引用返回还是const左值引用返回其实本质上都是引用那块空间,而这块空间出了作用域就销毁了。销毁了以后我们还拿到这个别名的话,那么问题就大了,因为就相当于野指针了。我们没有权限去访问
所以这个地方在之前只能使用传值返回
而传值返回的话,如果编译器不加任何优化,那么func返回的时候要产生一个临时对象,这是一次拷贝构造,然后用这个临时对象在拷贝构造给s,这又是一次拷贝构造,代价实在太大了。如果这个str字节有十几万的话,代价很大的。所以编译器将这里给优化为了一次拷贝构造
为了方便讨论,我们使用下面的这个string
namespace Sim { class string { public: typedef char* iterator; iterator begin() { return _str; } iterator end() { 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); } // s1.swap(s2) void swap(string& s) { std::swap(_str, s._str); std::swap(_size, s._size); std::swap(_capacity, s._capacity); } // 拷贝构造 string(const string& s) :_str(nullptr) { cout << "string(const string& s) -- 深拷贝" << endl; string tmp(s._str); swap(tmp); } // 赋值重载 string& operator=(const string& s) { cout << "string& operator=(string s) -- 深拷贝" << endl; string tmp(s); swap(tmp); 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]; 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) string& operator+=(char ch) { push_back(ch); return *this; } const char* c_str() const { return _str; } private: char* _str; size_t _size; size_t _capacity; // 不包含最后做标识的\0 }; }
当我们使用如下代码的时候,我们可以看到代价还是比较大的
Sim::string func() { Sim::string str("xxxxxxxxxxxxxxx"); return str; } int main() { Sim::string ret = func(); Sim::string s; s = func(); return 0; }
这里的第一次深拷贝是编译器优化后的,将两次深拷贝合二为一为一次深拷贝
而下面是我们先定义一个string变量,然后我们去使用func去创建一个string对象,然后返回他,此时我们只能去构造一个临时对象,这是一次拷贝构造,然后我们将这个临时对象赋值给s对象,由于我们的赋值运算符重载复用了拷贝构造,所以最终的代价是两次拷贝构造
我们先不管上面的,先看下面的代码
下面的代码是否构成函数重载?
当然构成函数重载,参数不同的类型
那么下面的是否构成函数重载呢?
其实也会的。
但是这里const左值引用是可以引用右值,而下面的也能引用右值。那么当我们写出下面的这段代码的时候,会发生什么呢?
他们会走向最匹配的函数
而且如果没有下面的,编译器也是可以跑的
所以在这里,如果有右值引用的版本,就会走右值引用版本,会走最匹配的
然后现在我们再去回过头来看前面的代码,我们知道,前面的代码在编译器不优化的情况下,代价有点太大了
在这里一共涉及到了三次深拷贝,第一次是str创建的时候,第二次是str返回的时候会产生一个临时对象,第三次是将这个临时对象给s时候,还要发生一次深拷贝。
然而其中有两块空间可以说是浪费掉了,如下图打×的部分都是被浪费掉了
那么有没有什么办法可以进行优化呢?
其实关于右值:
我们可以把它分为内置类型的右值和自定义类型的右值
而内置类型的右值我们一般也称为纯右值,自定义类型的右值一般也称为将亡值
而函数返回值,表达式返回值也是一个右值。并且对于我们上面s字符串的操作,比如说s1+s2,to_string,其实本质都是函数调用,(s1+s2是一个运算符重载,其实本质也是函数调用),而这些函数调用返回的都是将亡值。
也就是说
s = 左值 //只能老老实实进行深拷贝 s = 右值将亡值 //可以进行移动拷贝
这里的移动拷贝其实就是交换资源,如下所示,就是移动拷贝。
string& operator=(string&& s) { cout << "string& operator=(string&& s) -- 移动拷贝" << endl; swap(s); return* this; }
我们来分析一下,由于func函数返回的是一个临时对象,这个临时对象就是一个右值,既然是右值,那么我们就使用右值引用,正好交换资源。即可,相比使用const左值引用要减少了一次深拷贝。
使用const左值引用的话,const对象无法被修改,所以只能去使用一次拷贝构造去创建一个可以交换资源的对象,然后再进行交换。而对于右值引用,就不存在无法被修改的问题了。所以可以直接去交换资源
而且由于编译器会自动走最匹配的,所以对于右值会走向第二个函数,只有当第二个函数不存在的时候,才会走向第一个函数
如下图所示,第一次深拷贝是func函数中要返回一个临时对象所造成的,第二次拷贝是移动拷贝所必须的,但是这里的移动拷贝里面仅仅只是交换资源,几乎没有消耗。
所以使用了移动拷贝的话,那么就只剩下两次消耗了,一次是func函数中要开一个str字符串,一次是要返回一个临时对象,两次消耗,但是只有一次深拷贝。在赋值这里就没有任何消耗了。而原来的就是三次消耗,即需要两次深拷贝。
那么上面的场景是我们不让编译器优化的场景,那么如果让编译器优化呢?
我们可以在利用右值引用写出这样的拷贝构造函数
string(string&& s) :_str(nullptr) { cout << "string(string&& s) -- 移动拷贝" << endl; swap(s); }
反正右值都是将亡值,没什么用的值,那么不妨直接用来作为资源即可,即直接交换,直接就可以几乎没有任何开销了,没有深拷贝了
同样的,原来的const左值引用,虽然可以引用右值,但是由于const,导致我们无法直接利用这个将亡值的资源,我们只能眼睁睁看着这个将亡值自己消亡,却无法直接拿走他的资源。所以只能自己去利用它创造一个对象,用这个新的对象去交换资源,这样就多了一次深拷贝了。
可见直接0开销了,而前面编译器优化后还有1次开销呢,可见充分利用了右值的将亡特性。
不过在这里还有一些疑惑的点就是,func要返回的时候,str是一个左值,那么他就必须得用来拷贝构造来构造一个临时对象,这个临时对象确实可以零开销的进行拷贝构造了,但是这里应该还有一次拷贝构造啊?为什么打印结果里没有呢?
其实本身编译器就会做出一些优化:即连续的构造、拷贝构造会进行合二为一,甚至是合三为一。而编译器在这里做出的优化其实就是直接用str去拷贝构造ret。即不需要借助中间的临时变量了。那么func返回的其实就是str本身。而str虽然是一个左值,但是他本身符合将亡值的特征。因为出了作用域,它即将销毁,所以编译器此时做出了第二个优化:把str本身识别为右值。相当于给move了一下,右值引用去引用左值。
总的来说编译器直接进行了两次优化
- 连续的构造、拷贝构造合二为一(不需要临时变量了,只有传值返回才可以)
- 编译器把str识别为了右值(因为str虽然是左值,但是符合将亡值特征,相当于进行了一次特殊处理)
而且还需要注意的是,这里千万不可以写传引用返回
首先就是传引用返回的话,那么这块空间已经被销毁了,就出现了野指针的问题了。其次只有传值返回的时候,编译器才会进行优化,如果不传值返回,编译器就不会进行上面的优化了。
而且传值返回所造成的第一个优化,即不需要临时变量的本质其实就是把拷贝放在了str还没有销毁的时候,即在函数内部。而传引用返回就一定不可以了
在上面如果个str加上一共move,相当于我们也将他认为是右值了,这样其实也是可以的
一旦我们加上了拷贝构造的右值引用,那么对于编译器无法第一种优化的场景也可以使用第二次种优化。
在这里因为我们并不是连续的拷贝和拷贝构造。而是一次拷贝构造和一次赋值运算符重载。
拷贝构造是由于func要返回一个临时对象,但是这个我们可以将str识别为将亡值,就可以使用移动拷贝了。将str的资源转移到临时对象中去
然后这个临时对象又进一步的使用赋值运算符重载,这里又是一次移动拷贝,因为刚好这个临时对象是一个右值。又一次的转移资源。
最终整个过程没有任何的消耗,仅仅只是两次转移资源,代价极低
3.左值引用和右值引用的价值和场景
对于右值引用的移动拷贝,实际上我们一般只将其用于自定义类型中,尤其是深拷贝的场景,比如vector<vecor<string>>这种拷贝代价极大的场景,而对于内置类型,对其使用右值引用的移动拷贝其实意义并不是很大,或者说没有任何意义。并不能带来一丝的优化。甚至对于浅拷贝的自定义类型也没有任何价值。只有深拷贝的自定义类型才有价值。
左值引用的核心价值就是减少拷贝,提高效率
而右值引用的核心就是价值就是进一步减少拷贝,弥补左值引用没有解决的场景。如:传值返回。
那么右值引用的场景有哪些呢?
这个场景一就是:自定义类型中深拷贝的类,且必须传值返回的场景
而我们之前所演示的,正式满足上面两个条件的情形
像下面这个就不可以了,因为ret并不是右值。就只能老老实实拷贝构造了
我们可以同时对比满足和不满足的场景
此时str的地址后四位是d9d8
当出了移动拷贝结束后,此时ret1的地址后四位是d9d8
所以最终结果为交换了资源,而下面这个是不会交换资源的
像下面这种move也是不会转移走ret的资源的
但是move这样使用会转移走资源
我们可以这样去理解move,这个move会返回一个和ret一样的右值,它的资源都是一样的。所以才能导致调用移动拷贝
上面的不仅仅是我们实现的是这样的, 库里面的也是这样的会进行转移资源
场景二:容器的插入接口,如果插入对象是右值,可以利用移动构造转移资源给数据结构中的对象,也可以减少拷贝
如下图所示
在这里我们先来研究一下在何时发生的拷贝
如下所示,在尾插的时候,会先创建一个新节点,这个新节点在new的时候会调用它的拷贝构造,它的拷贝构造会走一个初始化列表,在这个初始化列表中调用了string中的拷贝构造,从而达到了深拷贝
而下面的尾插一共move后的str2时候,会调用右值引用,所以最终会调用移动拷贝,可以直接转移资源了
不过上面的写法会导致str2的资源被拿走
所以我们一般情况不会向上面那种写法
我们一般直接这样写,这样写也会调用移动拷贝,而且不会像中间那种写法使得str2的资源被转移走,导致str2失效
这样做的原理就是,3333333333333333这个字符串会先构造成一个临时对象或者说匿名对象,总之是具有常性的。是一个将亡值,就会导致它会去调用右值引用。
从而会去调用移动拷贝,减少拷贝,提高效率
如果没有右值引用的话,即没有移动构造的话,那么三个都将是拷贝+拷贝构造。(我们这里只是没有将拷贝打印出来而已)
即便是在STL的其他容器中,基本都是有右值引用的。用来提高效率
4.完美转发
万能引用
如下代码所示
下面代码是一个模板,在这个模板中,有一个看上去像右值引用的存在,但是实际上,它并不是右值引用,而是万能引用。
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<typename T> void PerfectForward(T&& t) { Fun(t); }
万能引用:它既可以接收左值,又可以接收右值
当实参为左值的时候,它就是左值引用,我们也称为引用折叠
当实参为右值的时候,它就是右值引用
所以对于下面的代码,我们就可以知道,这些实际调用的都是右值,左值,右值,const左值,const右值。他们调用的实际上不是同一个函数
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<typename T> void PerfectForward(T&& t) { Fun(t); } int main() { PerfectForward(10); // 右值 int a; PerfectForward(a); // 左值 PerfectForward(std::move(a)); // 右值 const int b = 8; PerfectForward(b); // const 左值 PerfectForward(std::move(b)); // const 右值 return 0; }
但是当我们运行的时候
运行结果为如下所示
我们会发现结果其实不符合我们的预期
这是什么情况?难道全折叠了?理论上应该不可能的吧。
我们先用下面这段代码来观察一下
我们发现这怎么也是左值引用呢?
我们可以用下面这段代码来发现一些问题。我们发现虽然r左值引用了a,rr右值引用了a,但是他们两个本身却是左值,因为他们可以取出地址。
我们知道,右值有两个属性:第一个是不可以取地址,第二个是不可以修改。
而这里rr不仅可以取地址,还可以进行修改。
而右值引用似乎却可以进行修改?其实它也必须得修改,如果它不支持修改,那么它就完蛋了
我们看我们实现右值引用的部分
我们会发现,我们在当func返回的这个临时变量对象进行调用移动拷贝的时候,这里s是右值引用了str,但是s居然可以被修改,而且这个s还可以传递给一个左值引用去修改。
所以说s是一个左值。
所以这里我们可以这样理解,虽然它是一个右值,我们也使用了右值引用,但是这个引用可以认为开了一块空间,把这个右值给存起来
所以说
右值引用变量的属性会被编译器识别成左值(相当于一个特殊处理)
否则在移动构造的场景下,无法完成资源转移,必须要修改
所以说这里的就是t无论它引用的是一个左值还是右值,它本身的属性就是一个左值
所以现在,我们再来看这里,我们就可以看懂了
那么如何让这个调用函数的时候,让它保持原有的属性呢?
C++在这里搞出来了一个完美转发
当t是左值的时候,保持左值属性
当t是右值的时候,保持右值属性
想到完美转发,我们可以突然意识到前面有一个问题似乎编译器底层应该用的就是完美转发了,即下面的val本来是左值,但是我们需要它的右值属性,所以可以使用完美转发
然后我们可以去尝试修改一下我们之前所是实现的链表,如下是之前的链表
namespace Sim { template<class T> struct list_node { list_node<T>* _next; list_node<T>* _prev; T _val; list_node(const T& val = T()) :_next(nullptr) ,_prev(nullptr) ,_val(val) {} }; 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->_val; } Ptr operator->() { return &_node->_val; } self& operator++() { _node = _node->_next; return *this; } self operator++(int) { self tmp(*this); _node = _node->_next; return tmp; } self& operator--() { _node = _node->_prev; return *this; } self operator--(int) { self tmp(*this); _node = _node->_prev; return tmp; } bool operator!=(const self & it) const { return _node != it._node; } bool operator==(const self & it) const { return _node == it._node; } }; //template<class T> //struct __list_const_iterator //{ // typedef list_node<T> Node; // Node* _node; // __list_const_iterator(Node* node) // :_node(node) // {} // const T& operator*() // { // return _node->_val; // } // __list_const_iterator<T>& operator++() // { // _node = _node->_next; // return *this; // } // __list_const_iterator<T> operator++(int) // { // __list_const_iterator<T> tmp(*this); // _node = _node->_next; // return tmp; // } // bool operator!=(const __list_const_iterator<T>& it) // { // return _node != it._node; // } // bool operator==(const __list_const_iterator<T>& 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_const_iterator<T> const_iterator; typedef __list_iterator<T, const T&, const T*> const_iterator; iterator begin() { //return _head->_next //单参数的构造函数支持隐式类型转换 return iterator(_head->_next); } iterator end() { return iterator(_head); } const_iterator begin() const { //return _head->_next //单参数的构造函数支持隐式类型转换 return const_iterator(_head->_next); } const_iterator end() const { return const_iterator(_head); } void empty_init() { _head = new Node; _head->_next = _head; _head->_prev = _head; _size = 0; } list() { //_head = new Node; //_head->_next = _head; //_head->_prev = _head; //_size = 0; empty_init(); } list(const list<T>& lt) { //_head = new Node; //_head->_next = _head; //_head->_prev = _head; //_size = 0; empty_init(); for (auto& e : lt) { push_back(e); } } void swap(list<T>& lt) { std::swap(_head, lt._head); std::swap(_size, lt._size); } list<T>& operator=(list<T> lt) { swap(lt); return *this; } void push_back(const T& val) { insert(end(), val); //Node* newnode = new Node(val); //Node* tail = _head->_prev; //tail->_next = newnode; //newnode->_prev = tail; //newnode->_next = _head; //_head->_prev = newnode; } void push_front(const T& val) { insert(begin(), val); } void pop_back() { erase(--end()); } void pop_front() { erase(begin()); } iterator insert(iterator pos, const T& val) { Node* newnode = new Node(val); Node* cur = pos._node; Node* prev = cur->_prev; prev->_next = newnode; newnode->_prev = prev; newnode->_next = cur; cur->_prev = newnode; ++_size; return newnode; } iterator erase(iterator pos) { assert(pos != end()); Node* cur = pos._node; Node* prev = cur->_prev; Node* next = cur->_next; delete cur; cur = nullptr; prev->_next = next; next->_prev = prev; --_size; return next; } size_t size() { //size_t sz = 0; //iterator it = begin(); //while (it != end()) //{ // it++; // sz++; //} //return sz; return _size; } ~list() { clear(); delete _head; _head = nullptr; } void clear() { iterator it = begin(); while (it != end()) { it = erase(it); } } private: Node* _head; size_t _size; }; }
当我们的代码与以前的链表相结合的时候,发现调用的全部都是深拷贝,而且还多了一次,深拷贝,多的那一次与我们的实现有关,因为链表里面有个头节点。
而对于STL库里里面的代码来说就是正常的移动拷贝了
主要原因就是因为,list的push_back接口只有const左值引用版本,为了解决这个问题,我们只能去使用一个右值引用版本的来处理
所以我们现在来进行修改list
首先是push_back中的
由于要调用insert,所以进一步修改
由于这里还涉及到Node,所以进一步修改
最后运行结果如下图所示
除此之外,我们还可以这样做,这样做的话,也就是说是,只需要使用一个万能引用就可以了。不过这个函数我们必须加上模板,不然的话对于const类型是无法进行构造的。
七、lambda表达式
1.对类数组排序的一个例子
如下代码所示,当我们想要对一个类中的数据进行排序的时候,我们想要使用sort的话,显然我们是无法直接进行排序的,当然我们可以使用运算符重载来支持直接排序,但是这里会出现一个问题,那就是一个类有很多的成员,我们如果想要对这个成员排序完成以后,还想要对其他成员进行排序,这个时候我们就只能使用仿函数了,来进行各种各样的排序,如下代码所示:
struct Goods { string _name; // 名字 double _price; // 价格 int _evaluate; // 评价 Goods(const char* str, double price, int evaluate) :_name(str) , _price(price) , _evaluate(evaluate) {} }; struct ComparePriceLess { bool operator()(const Goods& gl, const Goods& gr) { return gl._price < gr._price; } }; struct ComparePriceGreater { bool operator()(const Goods& gl, const Goods& gr) { return gl._price > gr._price; } }; struct CompareEvaluateGreater { bool operator()(const Goods& gl, const Goods& gr) { return gl._evaluate > gr._evaluate; } }; int main() { vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,3 }, { "菠萝", 1.5, 4 } }; sort(v.begin(), v.end(), ComparePriceLess()); //价格升序 sort(v.begin(), v.end(), ComparePriceGreater()); //价格降序 sort(v.begin(), v.end(), CompareEvaluateGreater()); //评价降序 }
但是上面代码还有一些问题,那就是假如一个命名不规范等问题出现的时候,会非常麻烦
有没有更好的办法呢?当然有,那就是lambda表达式
如下所示,就是一个lambda表达式的简单例子
如下所示也是一个简单的样子
总而言之:
函数指针 ------能不用就不用
仿函数---------类重载operator(),对象可以像函数一样使用
lambda表达式------匿名函数对象,函数内部可以直接定义使用。