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

上面说了那么多, 可能看的你脑壳越来越痛, 不要紧, 现在我们用代码来阐述. 比如下面这个dump_array类, 内部持有堆区的资源(也就是一个通过new分配的数组), 我们先给它把拷贝构造函数和析构函数实现掉.

#include <algorithm> // std::copy #include <cstddef> // std::size_t class dumb_array { public: // 构造函数 dumb_array(std::size_t size = 0) : mSize(size), mArray(mSize ? new int[mSize]() : nullptr) { } // 拷贝构造函数 dumb_array(const dumb_array& other) : mSize(other.mSize), mArray(mSize ? new int[mSize] : nullptr), { std::copy(other.mArray, other.mArray + mSize, mArray); } // 析构函数 ~dumb_array() { delete [] mArray; } private: std::size_t mSize; int* mArray; };

我们先来看一个失败的=操作符重载实现

// the hard part dumb_array& operator=(const dumb_array& other) { if (this != &other) // (1) { delete [] mArray; // (2) mArray = nullptr; // (2) *(see footnote for rationale) mSize = other.mSize; // (3) mArray = mSize ? new int[mSize] : nullptr; // (3) std::copy(other.mArray, other.mArray + mSize, mArray); // (3) } return *this; }

表面上看这个实现好像也没什么大问题, 但实际上它有三个缺陷:

在(1)处, 需要首先判断=操作符左右是不是同一个对象. 这个逻辑上来讲其实没什么问题. 但实际应用中, =左右两边都是同一个对象的可能性非常低, 几乎为0. 但这种判断你又不得不做, 你做了就是拖慢了代码运行速度. 但坦白讲这并不是一个什么大问题.

在(2)处, 先是把旧资源释放掉, 然后再在(3)处进行新资源内存的再申请与数据拷贝. 但如果第(3)步, 申请内存的时候抛异常失败了呢? 整个就垮掉了.一个改进的实现是先申请内存与数据拷贝, 成功了再做旧资源的释放, 如下

dumb_array& operator=(const dumb_array& other) { if (this != &other) { std::size_t newSize = other.mSize; int* newArray = newSize ? new int[newSize]() : nullptr; std::copy(other.mArray, other.mArray + newSize, newArray); delete [] mArray; mSize = newSize; mArray = newArray; } return *this; }

整个=重载的实现, 几乎就是抄了拷贝构造函数中的代码(虽然在本例中不是很明显: 因为拷贝构造函数中使用了成员初始化列表).

看到这里你可能觉得我在吹毛求疵, 但你稍微想一下, 如果我们要管理的资源的非常复杂的初始化步骤的话, 上面的写法其实就很恶心了. 首先是异常安全的保证就需要非常小心, 其次就是抄代码的情况就会非常明显: 同样的逻辑, 你要在拷贝构造函数和=操作符重载里, 写两遍!

那么一个正确的实现应当怎么写呢? 我们上面说过, copy-and-swap套路能规避掉上面的三个缺陷, 但在继续讨论之前, 我们首先要实现一个swap函数. 这个swap函数是如此的重要与核心, 我甚至愿意为此, 将所谓的rule of three改名叫成rule of three and a half, 其中的a half就是指这个swap函数. 多说无益, 我们来看swap的实现, 如下:

class dumb_array { public: // ... // 首先, 这是一个函数, 只是声明与实现都放在了类定义中, 而不是一个成员函数 // 其次, 这个函数不抛异常 friend void swap(dumb_array& first, dumb_array& second) { // 通过这条指令, 在找不到合适的swap函数时, 去调用std::swap using std::swap; // 由于两个成员都是基础类型, 它们没有自己的 swap 函数 // 所以这里调用的是两次 std::swap swap(first.mSize, second.mSize); swap(first.mArray, second.mArray); } // ... };

这个swap的实现初看起来很平平无奇, 其目的也十分显而易见(交换两个对象中的所有成员), 但实际上, 上面这个写法里也是有一些门道的, 限于篇幅关系, 这里不会掰开揉碎细细讲, 你最好仔细琢磨一下这个swap的写法, 比如:

为什么它非要写成friend void swap, 而不是写成一个普通函数

里面那句using std::swap有什么玄机? 想一想, 如果dumb_array的成员变量不是基础类型, 而是一个类类型, 并且这个类类型也完整的实现了rule of three and a half, 会发生什么?

总之, 现在先不关心swap实现上的一些细节, 仅仅只需要关注它的功能即可: 它是一个函数, 它能完成两个dumb_array对象的交换, 而所谓的交换, 是交换两个对象的成员的值.

在此基础上, 我们的=操作符重载可以实现成下面这样:

dumb_array& operator=(dumb_array other) // (1) { swap(*this, other); // (2) return *this; }

是的, 就这是么简洁. 你没有看错, 就是这么有魔力! 那么, 为什么说它规避了我们先前提到的三个缺陷呢? 它又是如何规避的呢?

首先再回顾一下, 我们实现=操作符重载的逻辑思路:

在fuck = shit的内部, 我们先将shit拷贝一份, 称其为shit2好了

然后使用一个swap函数, 将fuck与shit2进行交换: 即交换两个对象的所有成员变量的值. 这样就达到了"把shit的值赋给fuck"的目的

第三步, 在=操作符实现的内部退栈的时候, shit2会自动由于退栈而被析构.

整个过程没有风险, 没有异常, 很是流畅.

这里有几个点也需要额外说明一下:

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

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