move_semantics_cpp
C++移动语义&智能指针
摘自https://www.learncpp.com/cpp-tutorial/
在一个局部函数内部new一个对象,我们容易忘记对它delete,从而造成内存泄漏:
1 |
|
对此,我们可以采用RAII编程范例:通过局部变量的自动析构性,定义一个Auto_ptr1类:
1 |
|
将指针包含在类里,通过类的析构函数,可以做到自动释放内存。
但是这么做会有问题,例如两个Auto_ptr1可能管理同一个指针:
1 | int main() |
这样会对同一个地址delete两次,造成ub。
同样地,如果将Auto_ptr1作为函数参数或者函数返回值,都会出现奇怪的问题。
因此一种解决方式,是采用移动语义:通过转移ownership来替代浅拷贝:
1 |
|
这其实就是早期 std::auto_ptr
的写法。但是这个东西是很危险的,以至于在C++17标准中被删除。例如你将auto_ptr传入函数(并在函数结束时析构),但是调用者函数的对应的auto_ptr并不知道自己维护的指针已经释放了。二是auto_ptr没办法采用数组delete(挖坑,数组delete和普通delete的区别),三是auto_ptr没办法与STL容器很好地交互。
介绍移动语义之前,需要了解一下右值引用的性质。
要知道,函数的返回值是右值(然后用赋值=
赋给左值),而右值引用可以延长返回值的生命周期:
1 |
|
那,考虑将右值和右值引用作为函数参数,会是什么情况呢?
1 |
|
可以发现,当我们传入一个右值5,会采用形参为右值引用的重载函数。相比于const的左值引用(可以用右值初始化const左值引用),编译器认为右值引用是更好的选择。
那传入右值引用呢?
1 |
|
我操,居然右值引用是左值。这说明 int&& ref
是
int&&
类型的左值。
另外,根据learncpp,和不能返回左值引用一样,你也不能返回右值引用(虽然本身返回的是int&&类型的左值,但是它引用的是一个生命周期已经结束的右值,这很危险)
我们知道,const左值引用可以接受右值为参数。这里看看learncpp的copy类型auto_ptr:
1 |
|
按理来说应该有六行,但是我的编译器忽略了返回值。
挖坑,
Auto_ptr3& operator=(const Auto_ptr3& a)
前面引用的含义&
,它防止又调用一次copy constructor
这样我们就实现了一个基于copy的智能指针。但是,通过移动语义,可以做的更好。
C++11为类提供了移动构造函数和移动赋值重载,它们的参数都是右值引用:
1 |
|
认真看一下移动语义的实现~(挖坑:noexcept
由于Resource自始自终都只有一个(只不过所有权转移了很多次),因此只有一次构造和一次析构。
我们将移动语义与右值挂钩。这是因为右值一般来讲都是暂时使用的东西,在以后的程序执行不需要使用。因此比起前面使用左值来实现移动语义,右值更加安全。我们不希望类似于
a=b
这种东西会影响到 b
。
我们观察到,函数 generateResource
返回的是一个左值,但是似乎根据结果来看,返回过程采用了移动语义(即没有构造新值)。这是可以的,C++内部也是这么干的。因为返回的左值在离开函数后会立即销毁,我们没必要在这里使用深拷贝。
learncpp网站上给出建议:对于move-enabled的类,有时候禁止使用拷贝构造和拷贝赋值会更好:
1 |
|
这个东西非常像 std::unique_ptr
。正如其名,unique
的性质让它不能被复制。
std::move
用于将左值转化为右值,这样可以使用移动语义:
1 | int main() |
但是原有的字符串b变为了空值,这也是意料之内的。
但是根据learncpp上的建议:不要对任何被std::move后的对象的值有任何假设
因此我们以后要避免依赖b的具体的值的操作。