目录
序
先简单说一下右值引用
右值引用的基础知识,在这篇文章中说的很清楚,建议仔细阅读。
https://www.ibm.com/developerworks/cn/aix/library/1307_lisl_c11/index.html
简单来说,右值主要实现了转移语义和完美转发。一个是避免了拷贝,一个是使得将一组参数原封不动的传递给另一个函数方便实现。
此文搁置的时间比较久了,现在把图片补齐重新发一下。
过程
我们容易写出以下简单的类:
class test { public: test() { puts("construct"); } test(const test& t) { puts("copy construct"); } test(test&& t) { puts("move construct"); } ~test() { puts("destruct"); } test& operator=(const test& t) { puts("assignment"); return *this; } test& operator=(test&& t) { puts("move assignment"); return *this; } };
此类实现了最基本的默认构造函数、拷贝构造函数、移动构造函数、赋值运算符、移动赋值运算符。这里只是说明一下各函数声明。
将其丰富一下,让 test 类持有资源:
class test final { private: char* _data; size_t _len; void _init_data(const char* s) { _data = new char[_len + 1]; memcpy(_data, s, _len); _data[_len] = 0; } public: test() : _data(nullptr), _len(0) { puts("construct"); } test(const char* s) { _len = strlen(s); _init_data(s); } test(const test& t) { _len = t._len; _init_data(t._data); puts("copy construct"); } test(test&& t) { _len = t._len; _data = t._data; t._data = nullptr; puts("move construct"); } ~test() { if (_data) { delete _data; } puts("destruct"); } test& operator=(const test& t) { if (this != &t) { _len = t._len; _init_data(t._data); } puts("assignment"); return *this; } test& operator=(test&& t) { if (this != &t) { _len = t._len; _data = t._data; t._data = nullptr; } puts("move assignment"); return *this; } };
初始化问题
如果我们初始化一个具有一个test实例的vector,有两种方法:
// method A vector v{test()}; // method B vector<test> v; v.push_back(test());
第一种使用 initializer_list ,第二种使用默认构造方法后 push_back 。
对于第一种,使用的构造函数为
vector( std::initializer_list<T> init, const Allocator& alloc = Allocator() );
这里会发生一次拷贝,具体原因我现在无法准确讲清楚。initializer_list 虽然按值传递,但在传入函数体过程中并不会发生深拷贝。
对于第二种,使用的 push_back 函数为
void push_back( T&& value );
因为重载了移动构造函数,这里直接调用它,不发生拷贝。
vector 扩容问题
为了方便观察拷贝次数,现在对代码做一点修改,使得拷贝时会在字符串后面附加一个下划线(并不是一个好做法)。
#include <bits/stdc++.h> using namespace std; class test final { private: char* _data; size_t _len; void _init_data(const char* s) { _data = new char[_len + 10]; memcpy(_data, s, _len); _data[_len] = 0; } public: test() : _data(nullptr), _len(0) { printf("construct: %s\n", _data); } test(const char* s) { _len = strlen(s); _init_data(s); } test(const test& t) { _len = t._len; _init_data(t._data); _data[_len++] = '_'; printf("copy construct: %s\n", _data); } test(test&& t) { _len = t._len; _data = t._data; t._data = nullptr; printf("move construct: %s\n", _data); } ~test() { printf("destruct: %s\n", _data); if (_data) { delete _data; } } test& operator=(const test& t) { if (this != &t) { _len = t._len; _init_data(t._data); } puts("assignment"); return *this; } test& operator=(test&& t) { if (this != &t) { _len = t._len; _data = t._data; t._data = nullptr; } puts("move assignment"); return *this; } }; int main() { vector<test> v; v.push_back(test("A")); v.push_back(test("B")); v.push_back(test("C")); return 0; }
不难看出,此处的 vector 在扩容时疯狂进行拷贝。
为什么 vector 在此处扩容时会选择复制而不是移动呢?我们能不能让他移动?
考虑一下情景:假设我们使用移动策略,那么如果 vector 在扩容后,在移动的过程中移动构造函数抛出异常。此时问题将变得难以处理,原位置的资源已经被移动,而 vector 需要保证异常后还能维持数据不变。
解决方法也很简单,给移动构造函数加上 noexcept。注意,必须当此函数不可能抛出异常方可。
test(test&& t) noexcept { _len = t._len; _data = t._data; t._data = nullptr; printf("move construct: %s\n", _data); } test& operator=(test&& t) noexcept { if (this != &t) { _len = t._len; _data = t._data; t._data = nullptr; } puts("move assignment"); return *this; }
可以看到此时已不存在多余的拷贝。
本作品使用基于以下许可授权:Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.