一、move的概念
1、在学习move之前需要知道左值、右值、左值引用、右值引用的概念,见:https://www.cnblogs.com/judes/p/15159463.html
学习之后需要知道一个重点:
移动构造不进行深拷贝,直接使用右值的资源。【move是用来服务于此重点的】
2、概念
move将一个左值强制转化为右值,继而可以通过右值引用使用该值。
原型:
template<typename _Tp> constexpr typename std::remove_reference<_Tp>::type&& move(_Tp&& __t) noexcept { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }
其中:remove_reference<T>::type是去除T自身可能携带的引用。
注意1:move是一个常量表达式函数,没有做啥高级的操作,单纯的是将一个左值强制转化为右值;
注意2:右值其实就是之前“临时变量”的概念;
注意3:通过move一个变量a,只是返回一个右值,这个右值是根据a转换来的,执行move(a)后,对变量a没有任何改变,这是关键所在,也是让人迷糊的地方;
注意4:高级的地方是在需要使用到复制对象【类的对象,如果是单纯的基本数据如int、double则无用处,因为这些本身就是右值,也不存在构造】的时候,move用来配合生成右值,右值可用于调用移动构造函数;
二、帮助理解
甲有一本电子书,乙需要这本书,有多个方法实现这个场景。
①、甲将书拍照,而且只拍目录给乙,乙只能看个大概【对应浅拷贝】;
②、甲将书每一页都手抄下来,抄完之后给乙【对应深拷贝,深拷贝构造函数都是const引用,但这里实际上应该是乙来抄,因为是在乙的构造函数里深拷贝】;
③、甲将电子书复制一个副本【右值】发给乙,乙的任何操作不会影响甲的电子书【对应移动构造,输入右值】
综上我们引入move的目的流程就是:
我们需要右值【因为最后一步的需求】--------->move提供左值转右值强制转换功能--------->用于服务移动构造函数--------->移动构造里里直接将右值对象的资源给本对象。
三、矛盾点
如果使用普通浅拷贝,那本对象只能与原来对象共用资源;
如果使用深拷贝,那本对象会复制原对象所有资源,会消耗性能;
解决此矛盾点:
通过原对象生成一份右值,通过此右值来初始化本对象,此本对象不需要深拷贝,浅拷贝即可,并且不影响原对象资源。而这个右值初始化对象操作我们一般放在移动构造里实现。
四、使用
1、用右值初始化对象
①、使用普通构造
#include <iostream> #include <vector> class B { public: B() { } B(const B&) { } }; class A { public: A(): m_b(new B()) { std::cout << "A Constructor" << std::endl; } A(const A& src) : m_b(new B(*(src.m_b))) { std::cout << "A Copy Constructor, new something" << std::endl; } A(A&& src) noexcept : m_b(src.m_b) { src.m_b = nullptr; std::cout << "A Move Constructor, don't new anything" << std::endl; } ~A() { delete m_b; } private: B* m_b; }; static A getA() { A a; return a; } int main() { A a = getA(); A a1(a); //A a1(std::move(a)); return 0; }
调用了拷贝构造函数,实现了深拷贝,耗费了性能,打印:
②、使用移动构造
#include <iostream> #include <vector> class B { public: B() { } B(const B&) { } }; class A { public: A(): m_b(new B()) { std::cout << "A Constructor" << std::endl; } A(const A& src) : m_b(new B(*(src.m_b))) { std::cout << "A Copy Constructor, new something" << std::endl; } A(A&& src) noexcept : m_b(src.m_b) { src.m_b = nullptr; std::cout << "A Move Constructor, don't new anything" << std::endl; } ~A() { delete m_b; } private: B* m_b; }; static A getA() { A a; return a; } int main() { A a = getA(); //A a1(a); A a1(std::move(a)); return 0; }
调用了移动构造函数,只需要浅拷贝,打印:
2、高效交换值
template <class T> void MoveSwap(T & a, T & b) { T tmp = move(a); //std::move(a) 为右值,这里会调用移动构造函数 a = move(b); //move(b) 为右值,因此这里会调用移动赋值运算符 b = move(tmp); //move(tmp) 为右值,因此这里会调用移动赋值运算符 } template <class T> void Swap(T & a, T & b) { T tmp = a; //调用复制构造函数 a = b; //调用复制赋值运算符 b = tmp; //调用复制赋值运算符 }
五、实际的作用【纯个人理解】
①、对于单个的或指定数量的对象复制处理中,不需要引入这种晦涩难懂的玩意儿,好处是可以理解左值、右值,也就是以前的临时变量概念【现在这个概念被淡化了,右值概念来替代】,减少临时变量的生成,写多了反而让队友看不懂。【本人学习move这概念花了2周左右,因为其牵扯了左右值、移动构造、深浅拷贝等概念,实际上的move就是个常量表达式】
②、批量的复制,如自定义一个类,这个类存储的是我们需要处理任务的最小单元,也就是生产者消费者模式里的单个任务,里面的属性可能有指针,指针涉及到初始化。当把多个这样的类对象放在类似于vector这样的连续性容器里时,一旦在倒数n处的位置进行push或insert时,此位置之后的所有对象都得往后移动一位,即产生nx1次拷贝,时间复杂度是O(n);针对这种情况,我们就可以为类设计一个移动构造函数,并实现资源的重新指向,如此之后在进行push时,我们每次都是用前面一个对象的右值来初始化下一个位置的对象,时间复杂度为O(1)【考虑首尾的话就是O(2)?】,效果不言而喻。
PS:
1、右值引用可以用来改变右值,它是一个左值
class A{ ..... }; A a; A b = std::move(a);//这里并没有看到右值引用,单纯的用右值来调用移动构造初始化b,移动构造的入参就是右值引用
std::move(a)是生成一个右值,继而可以用右值引用,右值转化为右值引用是通过函数入参或直接声明来实现的:
方式1: A&& rf = std::move(a); 方式2: void fun(A&& rf) { rf.xxxx = nullptr; }
其中方式2可以是普通函数,但更常见在于移动构造函数、移动赋值函数【我更喜欢统称移动构造函数】
所谓的改变右值,一般就是将这个右值里指针【一般有指针的时候才会触发这种情景】置为空,这就达到了将右值的资源全转为本对象
2、move不改变原始值,但一般跟原始值即将不存在的情况使用
如函数返回值初始化对象、vector的push操作、两值交换等