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

首先, 这个=操作符重载的实现, 其参数是值类型, 而不是const dumb_array & other. 这里面是有门道的, 如果采用引用类型, 如下所示:

dumb_array& operator=(const dumb_array& other) { dumb_array temp(other); swap(*this, temp); return *this; }

从功能上看, 和先前的值引用版本没什么区别. 但内在上, 你实质上放弃了一个"让编译器自动优化代码"的契机. 这个细节展开来说也比较复杂, 具体缘由在这里 有详细解释, 但总结起来就是: 在C++中, 普通的拷贝操作(调用拷贝构造函数), 比起在函数传参时, 编译器在背后执行的拷贝操作(虽然从表面看它也是在调用拷贝构造函数), 效率要低, 并且还低得多!

使用值传递来自动使编译器在背后调用拷贝构造函数(实质上编译器会做一些优化, 但你可以这样理解), 保证了只要执行流程进入到了=操作符的内部, 数据拷贝就已经完成了. 这暗地里还复用了拷贝构造函数的代码. 所以, 代码重复的问题解决了.

并且由于这样的写法, 只要函数调用这个动作被成功发起了, 就代表着数据拷贝已经成功: 这意味着拷贝过程中发生的内存分配等其它高危操作已经完成, 如果有异常, 应当在函数调用之前被扔出来, 而一旦代码执行进调用内部, 就不可能再抛异常了. 这解决了异常安全的问题

我们也规避了用以检查=左右两边是否为同一个对象的逻辑. 虽然如果这种情况发生, 这种写法会导致一次额外的数据拷贝与析构, 但这也是可以接受的, 毕竟, 如果出现了这种情况, 你应当反思的是为什么出现了自己 = 自己这种奇怪的逻辑, 而不是去苛责自己 = 自己执行的不够快.

至此, 就是copy-and-swap套路的所有内容.

那么, 在C++11中, 事情发生了任何变化了吗? 我们在rule of three/five这一小节说过, 由于C++11引入了右值引用和移动语义, 所以three变five: 你要新增一个移动构造函数, 与右值引用版的=操作符重载. 但实质上, 使用copy-and-swap套路的话, 你并不需要为=操作符再写一个右值引用版本的重载, 你只需要像下面这样, 添加一个移动构造函数就可以了:

class dumb_array { public: // ... // 移动构造函数 dumb_array(dumb_array&& other) : dumb_array() // 调用默认构造函数, 这在本例中不是必须的. { swap(*this, other); } // ... };

关于为什么不需要再写一个右值引用版的=操作符重载, 这个, 你可以先了解一下下一节的内容: 移动语义后, 再来看这里. 总之, 就是, 使用copy-and-swap套路, 在C++11中, 可以将所谓的rule of five变成rule of four and a half, 分别是:

1. 析构函数 2. 移动构造函数 3. 拷贝构造函数 4. `=`操作符重载 4.5. `swap`函数 移动语义

要理解移动语义, 其实用代码说话更容易让人理解. 我们就从下面的代码片断开始: 这是一个非常简单简陋的string的实现(注意它不是标准库中的std::string, 这里仅是我们自己实现的一个非常简陋的字符串类), 它内部使用一个指针成员持有着数据:

#include <cstring> #include <algorithm> class string { char* data; public: string(const char* p) { size_t size = strlen(p) + 1; data = new char[size]; memcpy(data, p, size); }

由于我们在这个简陋的实现里选择使用指针来管理数据, 即是作为类的设计者, 我们需要手动管理具体数据占用的内存的分配与释放, 所以按C++03标准的最佳实践, 我们应当遵循rule of three. 即是: 析构函数, 拷贝构造函数, =操作符的重载三者必须同时在场. 我们先在这里把析构函数和拷贝构造函数补上, 关于=的重载, 后面一点再谈

~string() { delete[] data; } string(const string& that) { size_t size = strlen(that.data) + 1; data = new char[size]; memcpy(data, that.data, size); }

拷贝构造函数实现了拷贝的语义, 参数const string & that是const引用, 这代表着它可以指向C++03标准中的右值, 即是一个表达式的值的最终类型是为上面这个简陋的string, 都可以作为拷贝构造函数的参数使用. 所以, 在假定我们还实现了类似于标准库中std::string对+的重载的话, 我们可以以如下三种方式调用拷贝构造函数:

... // x和y是两个string类型的变量 string a(x); // Line 1 string b(x + y); // Line 2, 这里假设我们实现了+的重载, 使得表达式 x + y 的类型也是 string string c(some_function_returning_a_string()); // Line 3

现在就到了理解移动语义的关键点:

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

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