C++11:移动语义(Move Semantic)
2020-07-14更新:参考了其他博客,对内容进行补全。
浅拷贝、深拷贝
-
浅拷贝(shallow copy):按位拷贝对象,创建的新对象有着原始对象属性值的一份精确拷贝(但不包括指针指向的内存)。
-
深拷贝:拷贝所有的属性(包括属性指向的动态分配的内存)。换句话说,当对象和它所引用的对象一起拷贝时即发生深拷贝。
class Vector{ int num; int* a; public: void ShallowCopy(Vector& v); void DeepCopy(Vector& v); }; //浅拷贝 void Vector::ShallowCopy(Vector& v){ this.num = v.num; this.a = v.a;//拷贝后对象和原对象的指针指向相同对象 } //深拷贝 void Vector::DeepCopy(Vector& v){ this.num = v.num; this.a = new int[num]; for(int i=0;i<num;++i){a[i]=v.a[i]} }
可以看到,深拷贝的开销往往比浅拷贝大(除非没有指向动态分配内存的属性),所以我们就倾向尽可能使用浅拷贝。
但是浅拷贝有一个问题:当有指向动态分配内存的属性时,会造成多个对象共用这块动态分配内存,从而可能导致冲突。一个可行的办法是:每次做浅拷贝后,必须保证原始对象不再访问这块内存(即转移所有权给新对象),这样就保证这块内存永远只被一个对象使用。
那有什么对象在被拷贝后可以保证不再访问这块内存呢?答案是临时对象。
要解决这个问题,我们先来认识左右值。
左值和右值
C++98左右值的概念:
-
左值(lvalue) :表达式结束后依然存在的持久对象。
-
右值(rvalue) :表达式结束后就不再存在的临时对象。
之所以取名左值右值,是因为在等式左边的值往往是持久存在的左值类型,在等式右边的表达式值往往是临时对象。字符串字面量是唯一不可算入右值的字面量,因为其代表字符数组,实际存储在静态数据区,这里的静态数据区是相对于堆栈等动态数据区而言,存放全局变量和静态变量的内存区。
C++11左右值被重新定义,使用下面两种独立的性质来区别类别:
-
拥有身份:指代某个非临时对象。
-
可被移动:可被右值引用类型匹配。
每个C++表达式只属于三种基本值类别中的一种:左值 (lvalue)、纯右值 (prvalue)、将亡值 (xvalue)
-
拥有身份且不可被移动的表达式被称作 左值 (lvalue) 表达式,指持久存在的对象或类型为左值引用类型的返还值。
-
拥有身份且可被移动的表达式被称作 将亡值 (xvalue) 表达式,一般是指类型为右值引用类型的返还值。
-
不拥有身份且可被移动的表达式被称作 纯右值 (prvalue) 表达式,也就是指纯粹的临时值(即使指代的对象是持久存在的)。
-
不拥有身份且不可被移动的表达式无法使用。
可归纳:
-
左值(lvalue) 指持久存在(有变量名)的对象或返还值类型为左值引用的返还值,是不可移动的。
-
右值(rvalue) 包含了 将亡值、纯右值,是可移动(可被右值引用类型匹配)的值。
如此分类是因为移动语义的出现,需要对类别重新规范说明。例如不能简单定义说右值就是临时值(因为也可能是std::move过的对象,该代指对象并不一定是临时值)。
右值引用
声明:左值引用声明符号为&,右值引用声明符号为&&。
//C++11中通过在某个类型后放置一个符号&&来声明一个右值引用,用于引用一个右值(即临时量) //声明 int&& a = 1; void Func(T&& rhs);
C++11引入右值引用,目的之一是为了支持移动操作。使用右值引用的思想,即通过移动语义实现浅拷贝,就解决了临时对象的问题,减少了原本使用深拷贝的开销。
移动语义
在对两个类型进行数据交换时,我们有时实际想要的是让A所拥有的资源转让给B,即转让资源所有权,而不是发生对象拷贝。
因此,移动语义的引入可以在进行大规模数据复制的时候,将动态申请的内存空间的所有权直接转让出去。
注意:使用移动语义意味着
-
原对象不再被使用,如果使用会造成不可预知的后果。
-
所有权转移,资源的所有权被转移至新的对象。
移动语义通过移动构造函数和移动赋值操作符实现,其特点如下:
-
参数的符号必须为右值引用符号,即为&&。
-
参数不可以是常量,因为函数内需要修改参数的值
-
参数的成员转移后需要修改(如改为nullptr),避免临时对象的析构函数将资源释放掉。
实例:
template <typename Object> class Vector { public: //移动构造函数 Vector(Vector&& rhs) noexcept : theSize{ rhs.theSize }, theCapacity{ rhs.theCapacity }, objects{ rhs.objects } { rhs.theSize = 0; rhs.theCapacity = 0; rhs.objects = nullptr; } //移动赋值函数 Vector& operator= (Vector&& rhs) noexcept { std::swap(theSize, std::move(rhs.theSize)); std::swap(theCapacity, std::move(rhs.theCapacity)); std::swap(objects, std::move(rhs.objects)); return *this; } private: int theSize; int theCapacity; Object* objects; }
标准库函数std::move
定义在头文件utility中。用于把任何左值(或右值)转换成右值,简单来说,它使一个值易于“移动”。但不会真正移动数据。
//std::move的函数原型定义 template <typename T> typename remove_reference<T>::type&& move(T&& t) { return static_cast<typename remove_reference<T>::type&&>(t);//强制转换类型为右值引用 }
下面以实现swap例程为例:
//通过3次复制的实现 void swap( vector<string> &x, vector<string> &y) { vector<string> tmp = x; x = y; y = tmp; }
这种实现会调用vector的拷贝赋值运算符进行拷贝,显然只适合小规模数据的交换,如果数据量稍大一点拷贝开销也十分巨大。
//通过3次移动的实现 void swap( vector<string> &x, vector<string> &y) { vector<string> tmp = std::move(x); x = std::move(y); y = std::move(tmp); }
这种实现方式将x,y,tmp通过std::move转换成右值后,再调用vector的移动赋值运算符进行移动,大数据情况下减少很大部分拷贝开销。
参考博客
[1]作者:KillerAery 出处:http://www.cnblogs.com/KillerAery/
[2]https://wendeng.github.io/2019/05/14/c++%E5%9F%BA%E7%A1%80/c++11std-move%E4%BD%BF%E7%94%A8%E4%B8%8E%E5%8E%9F%E7%90%86/