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

为了避免上面示例中的资源重复释放问题, 你需要自行实现对象的拷贝语义, 根据资源是否能被安全的重复释放, 或者资源是否能被安全的多个对象持有多份拷贝, 来决定拷贝的语义

为了实现拷贝的语义, 你需要自行实现拷贝构造函数与=操作符重载

所以一个安全的person类应当实现如下的拷贝构造函数和=操作符重载

// 拷贝构造函数 person(const person& that) { name = new char[strlen(that.name) + 1]; strcpy(name, that.name); age = that.age; } // =操作符重载 person& operator=(const person& that) { if (this != &that) { delete[] name; // 这其实是一个很危险的写法, 但如何正确的写一个=操作符重载并不属于本节所要讨论的范畴 // 所以暂时先可以凑合这样写着 name = new char[strlen(that.name) + 1]; strcpy(name, that.name); age = that.age; } return *this; }

注意上面的=操作符重载的实现是很不安全的, 但如何正确的写一个=操作符重载并不是本节所要讨论的内容(下一节"copy-and-swap idiom"中再进行讨论). 这里只要明白为什么析构函数, 拷贝构造函数, =操作符重载应当同生共死就行了.

某些场合中, 对象所持有的资源是不允许被拷贝的, 比如文件句柄或互斥量. 在这种场合, 与其费尽心机的去研究如何让多个对象同时持有同一个资源, 不如直接禁止这种行为: 禁止对象的拷贝与赋值. 要实现这种功能, 在C++03标准之前, 有一个很简单的方式, 即是把拷贝构造函数和=操作符在类内声明为private, 并且不去实现它们, 如下:

private: person(const person& that); person& operator=(const person& that);

C++11标准下, 你可以使用delete关键字显式的说明, 不允许拷贝操作, 如下:

person(const person& that) = delete; person& operator=(const person& that) = delete;

所以, 至此, 就基本说明白了为什么rule of three是大家自从C++98以来, 总结出来的一个最佳实践. 比较遗憾的是, 在语言层面, C++并没有强制要求所有程序员都必须这样写, 不然不给编译通过. 所以说呀, C++这门语言还真是危险呢.

而自C++11以来, 类内的特殊的成员函数由三个, 扩充到了五个. 由于移动语义的引入, 拷贝构造函数和=操作符重载都可以有其右值引用参数版本以支持移动语义, 所以rule of three就自然而然的被扩充成了rule of five, 下面是例子:

class person { std::string name; int age; public: person(const std::string& name, int age); // 构造函数 person(const person &) = default; // 拷贝构造函数 person(person &&) noexcept = default; // 拷贝构造函数: 右值引用版. 也被称为移动构造函数 person& operator=(const person &) = default; // =操作符重载 person& operator=(person &&) noexcept = default; // =操作符重载: 右值引用版 ~person() noexcept = default; // 析构函数 }; copy-and-swap idiom

在rule of three/five小节, 我们已经讨论了, 任何一个管理资源的类, 都应当实现拷贝构造函数, 析构函数与=操作符重载. 这三者中, 实现拷贝构造函数和析构函数的目的是很显而易见的, 但=操作符重载的实现目的, 以及实现手段在很长一段时间内都是有争论的, 人们在实践中发现, 要实现一个完善的=操作符重载, 其实并不像表面上想象的那么简单, 那么, 到底应当如何去写一个完美的=操作符重载呢? 这其中又有哪些坑呢? 这一节我们将进行深入讨论.

简单来说, copy-and-swap就是一种实现=操作符重载的最佳实践, 它主要解决(或者说避免了)两个坑:

避免了重复代码的书写

提供了强异常安全的保证

逻辑上来讲, copy-and-swap在内部复用了拷贝构造函数去拷贝资源, 然后将拷贝好的资源副本, 通过一个swap函数(注意, 这不是标准库的std::swap模板函数), 将旧资源与拷贝好的资源副本进行交换. 然后复用析构函数将旧资源进行析构. 最终仅保留拷贝后的资源副本.

上面这段话你看起来可能很头晕, 这不要紧, 后面会进行详细说明.

copy-and-swap套路的核心有三:

一个实现良好的拷贝构造函数

一个实现良好的析构函数

一个实现良好的swap函数.

所谓的swap函数, 是指这样的函数:

不抛异常

交换两个对象的所有成员

不使用std::swap去实现这个swap函数. 因为std::swap内部调用了=操作符, 而我们要写的这个swap函数, 正是为了实现=操作符重载

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

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