对C++11中的`移动语义`与`右值引用`的介绍与讨论 (5)

注意在第一行, 我们使用x作为参数去调用拷贝构造函数初始化a, 拷贝构造函数内部实现了深拷贝: 即完整的把x底层持有的数据拷贝了一份. 这没有任何毛病, 因为这就是我们想要的, 完成初始化a之后, a和x分别持有两份数据, 后续再对x做一些数据修改的操作, 不会影响到a, 反之亦然. x显然也是C++03标准中的左值.

而第二行和第三行的参数, 无论是x + y还是some_function_returning_a_string(), 显然都不能算是C++03中的左值, 显然它们都是右值. 因为这两个表达式的运算结果虽然确实是一个string的实例, 但没有一个变量名去持有这些实例, 这些实例都是临时性的实例: 阅后即焚. 即在这个表达式之后, 你没有任何办法再去访问先前表达式指代的那个string实例. 按照C++03的规范, 这种临时量占用的内存在下一个分号之后就可以被扔掉了(更精确一点的说: 在整个包含着这个右值的表达式单元执行完毕之后. 再精确一点: 编译器的实现是不确定的, 你应当假定在表达式执行完毕后这个对象就被析构了, 但编译器多数情况下只会在遇到下个}的时候才析构这种临时对象).

这就给了我们一个灵感: 既然在下个分号之后, 再也无法访问x + y与some_function_returning_a_string()这两个表达式指向的临时string对象, 意味着我们可以在下个分号之前(换句话说, 在初始化b和c的过程中: 在拷贝构造函数中), 随意蹂躏这两个临时量! 反正蹂躏完了也不会产生任何额外副作用.

基于这种思路, C++11标准中引入了一种新的机制叫右值引用, 右值引用一般用于函数重载(的参数列表)中, 它的目的是探测调用者传入的参数是否是C++03中的临时量. 一旦探测到调用者传入的是一个临时量的话, 重载调用机制就会匹配到有右值引用参数的重载中. 在这种函数内部, 你通过右值引用可以去访问这个临时量, 并在内部随意蹂躏这个临时量.

说起来有一点绕, 我们直接使用右值引用这个机制去写一个拷贝构造函数的重载, 如下所示:

string(string&& that) // string&& is an rvalue reference to a string { data = that.data; that.data = nullptr; }

在向string的内部添加了这个拷贝构造函数后, string类内部目前就有了两个拷贝构造函数: string(const string& that)与string(string&& that). 我们再回到上面的a, b, c三个初始化语句上. 这时, 由于x是一个左值, 所以a的初始化会匹配至string(const string& that). 而由于x + y与some_function_returning_a_string()是两个显然的临时量右值, 所以对于b和c的初始化, 就会匹配到string(string&& that).

那么string(string&& that)内部到底做了什么事情呢? 看上面的代码就很显然, 它并没有像string(const string& that)那样去真正的拷贝一份数据, 而仅仅是把临时量内部持有的数据偷了过来, 用读书人的说法, 就叫移动.

这里需要注意, 在string(string&& that)执行结束之后, 临时量x + y与some_function_returning_a_string()还是会和C++03一样, 阅后即焚. 这两个临时对象依然会被析构. 临时量始终都是临时量, 从C++03到C++11, 这个行为没有变化. 只不过, 在析构之前, 我们已经通过string(string&& that)把它内部的数据偷掉了! 真正这两个临时量被析构的时候, 执行的只不过是delete nullptr罢了.

恭喜你, 到目前为止, 理解了C++11中移动语义的基本概念.

现在, 在进一步讨论之前, 让我们先把string类的=操作符重载再补上. 根据C++03的最佳实践之copy and swap idiom, 一个行为正确异常安全的=操作符重载应当被实现成下面这样:

string& operator=(string that) { std::swap(data, that.data); return *this; } };

看到上面这个代码你是不是准备问我, "右值引用哪去了? ". 我的回答是: "这里并不需要右值引用", 至于为什么, 我们再来看下面三行代码:

// x, y, a, b, c 均是string类型的变量 a = x; // Line 4 b = x + y; // Line 5 c = some_function_returning_a_string(); // Line 6

我们先来分析第四行(Line 4).

由于string& operator=(string that)是值类型参数, 所以在调用发生时, 参数的传递会使用x先去初始化that, 你可以理解为string that(x)这种. 由于x是一个左值. 所以that的初始化使用的是string(const string& that)这个构造函数: 即that是x的一个完整副本, 深度拷贝了x的数据

在执行std::swap(data, that.data)的过程中, a持有的数据与that持有的数据相互交换. 至此, a持有的数据其实就是x数据的一个完整副本.

在return *this执行之后, that由于函数退栈, 被析构. that中持有的数据(其实是原a持有的数据)被析构函数安全释放

总结起来: a = x内部, 将x的数据完整的复制了一份给a, 再把a原持有的数据安全析构掉了.

我们再来分析第五行(Line 5)

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zzspdp.html