右值、右值引用的等相关的持续学习
恰好一年半这段时间没更新,其实不是啥好事,但另一方面又庆幸能来写一写。
概念都搞的比较模糊。持续杂记、整理下。
这记录的是当前(2024-11-21)的理解,有错的话持续修正。
三一法则五一法则
默认构造、拷贝构造、拷贝赋值、移动构造、移动赋值。这五个是编译器会默认合成的,且说他们的是否会合成规则就挺绕的了。它们一起构成了对象生命周期和内部数据的一些管理。涉及它们的有些是约定、有些是规定。
另管理资源的还有析构函数。
析构函数多半是要加virtual的,移动函数多半是要加noexcept的 。
左值、右值、右值引用。
右值引用也只是引用。
一个名字,它被定义为右值引用,引用到了某个对象,那这个名字的修改就会体现在这个变量上。
而右值引用,其实就是默认只能绑定到右值上的引用。
std::move
就是个强制类型转换。作用于一个非右值的实参后,让一个右值引用可以绑定到一个非右值上。
移动函数的书写约定
默认合成的是什么样的,和显式手写的有什么区别,和default修饰的,这三者之间关系。 default的和默认合成的,在实现本身上完全一致,但为什么default有啥用,
可以改变函数可访问性;
对复制的相关函数参数是否是const的改变;(移动的两个函数本身就必须是非const的形参)
增加可读性;
自定义的移动构造函数,当然你可以任意的写。但它存在的目的是能在某些重载被匹配时,使用起来更高效。如何高效,对于一些资源,在使用方已知并遵守规则情况下,对资源做“窃取”,为啥这样做,还是为了高效,因为使用方知道这个对象可以被窃取,所以让使用方有的选,可以选择走移动这条道,省掉不必要的复制代价。
复制控制的几个函数存在就是为了对一些特殊资源的管理。
如果都是普通资源,那直接各层级都走默认的就是合理且高效的。
而对于一些特殊资源,典型的例子就比如一块内存。
复制控制,合理适当的被使用场景是 当要深拷贝的地方,而移动发生在构造实参后面不在被使用的场景,(通常右值做为实参后不会再被使用,显示的std::move后的变量应该也被约定为不再被使用。)
对于移动,有了这些约定和使用规范后(需要使用者自己遵守),就可以再 移动构造函数或移动赋值操作符中,进行“窃取”之事了。仍以一块内存为例,那就是直接将实参的指针值赋值给当前被构造对象的对应属性,而不是new个新的再做深拷贝复制。
何时被调用
有了这些高效的 移动构造函数或移动赋值操作符 后,什么时候被调用呢,理论上当然是被优先调用为好了(当条件满足,能被匹配的前提下),这个应该就看重载匹配相关的知识点了。
除了这些收规矩的自动优化调用,显式约定调用的就是 强制类型转换,比如std::move来修饰下实参的情况下了。其实,还是走的重载匹配的路子,只是让这个实参做了 更深的修饰,让它符合被右值版本重载匹配的条件。
一些测试代码
通过类类型的属性,在起构造时,被调用的构造函数类型来观察:
#include <iostream> class Nest { public: Nest() { std::cout << "run in Nest()" << std::endl; } Nest(const Nest& robj) { std::cout << "run in Nest(const Nest &)" << std::endl; } Nest(Nest&& robj) /*noexcept*/ { std::cout << "run in Nest(const Nest &&)" << std::endl; } }; class X { public: // X() :pt(new char[3]) { // std::cout << "run in X()" << std::endl; // } X(int ,int){} //X(const X& robj) = default; //X(X && robj) = default; // X(X&& robj) /*noexcept*/ :pt(robj.pt) { // robj.pt = nullptr; // std::cout << "run in X(const X &&), curr robj.pt:" << (int)robj.pt << std::endl; // } X& operator=(const X&) = default; private: char* pt; Nest m_n; }; void funTestObj(X& robj) { std::cout << "run in funTestObj(X& robj)" << std::endl; } void funTestObj(X&& robj) { std::cout << "run in funTestObj(X&& robj)" << std::endl; } void funt1120(X&& robj) { //X newobj = robj; // 拷贝构造函数 X newobj = std::move(robj); // 拷贝构造函数 //funTestObj(robj); } int main() { //X obj; X obj(1, 2); //funt1120(std::move(obj)); X obj2 = std::move(obj); //移动构造函数 return 0; }
例如上面的版本,我们可以测试,当有这句时“X& operator=(const X&) = default;”,Nest类走的是拷贝构造;咩有的时候,能成功走移动。