序
先简单说一下右值引用
右值引用的基础知识,在这篇文章中说的很清楚,建议仔细阅读。
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.



