C++ 移动语义 理解
Related issues
- high correlation
- low correlation
std::move()本身的效果仅仅是类型上的转换(这里涉及到的就是左右值的问题),
但是和类的成员函数搭配后,就可以产生其他的效果,例如unique_ptr在完成move后,原始指针会被置为nullptr
还有一个明显的例子,parallel101/hw02中,询问为何将拷贝赋值函数设为delete后,程序并无错误,实际答案是程序中仅使用到了拷贝构造,并没有使用到拷贝赋值。但是产生了其他疑问,即假设程序确实使用到了拷贝赋值函数List c; c = b;
,虽然没有拷贝赋值函数,但是为什么不会去调用移动赋值函数,所以真的去测试了一下,测试结果是a是左值,但是移动赋值函数的形参是右值引用,因此不会去调用它。想要调用移动赋值函数,必须使用c = std::move(b)
,确实正确调用了移动赋值函数。除此之外,还能够发现此时的b作为一个List类型的对象,仍然是存在的,发生变化的是b.head,其数值变为了nullptr,而这并不是std::move()产生的效果,而是移动赋值函数产生的效果,正如上面所说的。
但是两部分的讨论反而会让人产生疑惑,原因在于,在左右值一文中,移动语义起到的作用是将左值显示转换为右值
但是在unique_ptr一文中,移动语义起到的作用是将原有指针置空,同时将对象的控制权移交给目标指针(所谓移交控制权,就是让目标指针指向目标对象)
从表面来看,两种用法毫不相关,所以才让人产生困惑。
std::move
在命名上给我们带来了困惑,因为“移动”这一词来看,它似乎完成了所有任务,这其中包括对象生命周期的控制权的转移,但实际上它仅仅完成了类型转换这一工作,例如从一个左值转向一个右值。在完成类型转换之后,由移动拷贝/赋值函数完成对象生命周期控制权的转移。也就是说,广义上的移动语义是由两个阶段组成的,分为了类型转换和生命周期控制权转移,那么应当如何理解这两种操作的必要性?
任何一种新事物的出现都有其内在的道理,一般情况是随着应用场景的不断丰富,利用现有机制不再能解决一些问题,因此促使了C++在拷贝之外又创造了移动语义。而这种问题实际指的是资源管理难题。
在C语言中,程序员手动对资源进行管理,很容易出现资源忘记释放的问题,在C++中通过RAII思想巧妙地解决了这一问题。
但RAII并非完美的,它带来了一些新的问题。类的拷贝是很常见的场景,由于RAII会自行完成类的析构,也就对应着自动完成了资源的释放,当管理资源的类发生拷贝时就很容易发生资源重复释放的问题,为了解决这一问题也就产生了三五法则,该法则的出现的确避免了这种问题。但是限制了拷贝操作,该如何面对资源控制权在不同类之间的转移问题。
面对这一问题,C++提出了移动语义,目的在于解决控制权在不同类之间的转移问题。而右值的产生是为了表示在转移过程中那些仅持有一段时间被管理者生命周期的变量,它们随时可能被剥夺这种控制权,不再具有意义,实际上是人为创造出的一种更加贴合应用场景的变量类型。
《C++ primer》的说法是,移动语义实现了移动对象,避免了拷贝对象,而必须使用移动而非拷贝的原因主要来自IO类和unique_ptr等不能进行拷贝的对象(似乎这里的对象和指针指向的对象并非同一概念)
移动语义一个比较好的理解场景是C++ 类成员函数全家桶
移动语义只是完成了右值引用的创建,因为右值本身就代表一个即将被销毁的生命周期较短的变量
移动构造和移动赋值形参都是右值引用类型
unique_ptr原始指针变为nullptr,并不是move造成的,而是unique_ptr本身作为一个类,是具备移动赋值运算符的自定义实现方式的,将原始指针赋值为nullptr是在函数实现时完成的
将这两个方面结合来看,就比较好理解为什么移动构造和移动赋值的参数都定为右值,
需要进行拷贝说明原始内容仍然需要长期保留,但是移动则意味着原始内容无需保留
移动语义就是为了解决某些不需要进行拷贝的场景,那么在进行构造或赋值时,原始指针或其他东西的值
和move联系最紧密的是左右值,底层机理也是左右值
但是实际应用涉及到的则是类的几个成员函数
unique_ptr所代表的智能指针看似是独立的一部分内容,但实际上它们也是类,所有的内在机理和外在表现都脱离不开左右值,移动语义和类的成员函数