手动创建将亡值
我们继续前面的函数传参的例子,但是我们做一些修改:调用方提供的不再是一个临时对象,而是一个普通的局部对象
1 2 3 4 5 6 7 8 9 10 11 12 13
| void ProcessBuf(Buffer buf) { for (int i = 0; i < buf.size(); i++) { buf.at(i) = 2 * i; }
for (int i = 0; i < buf.size(); i++) { std::cout << buf.at(i) << " "; } std::cout << "\n"; }
void test2() { Buffer a{5}; a.at(0) = 100;
ProcessBuf(a); }
|
程序运行结果如下(这里无所谓是否关闭优化,因为编译器并不敢进行优化)
1 2 3 4 5
| call constructor // 在test2函数中,构造局部变量a call copy constructor // 给ProcessBuf函数传参的过程,基于a拷贝构造参数对象buf 0 2 4 6 8 call destructor // ProcessBuf函数调用结束,参数对象buf自动析构 call destructor // test2函数调用结束,局部变量a自动析构
|
如果我们将ProcessBuf
函数的参数类型改为右值引用,程序会直接编译报错
1 2 3 4 5 6 7 8 9 10 11 12 13
| void ProcessBuf(Buffer &&buf) { for (int i = 0; i < buf.size(); i++) { buf.at(i) = 2 * i; }
for (int i = 0; i < buf.size(); i++) { std::cout << buf.at(i) << " "; } std::cout << "\n"; }
void test2() { Buffer a{5}; a.at(0) = 100;
ProcessBuf(a); }
|
原因很简单,我们在函数传参中相对于执行了如下语句
但是正如前文所说,右值引用不能绑定普通变量,只能绑定将亡值。
这在语法上其实是非常合理的:我们不能让右值引用buf
“夺舍”一个普通的变量,万一这个变量在函数调用之后还会使用呢?
但是如果我确实有这样的特殊需求:
- 明确保证普通变量在调用方这一侧已经完全不打算用了,主动将其视作将亡值;
- 我希望把
a
的内存控制权直接移交给ProcessBuf
函数中的buf
参数,从而节约传参过程的成本。
简而言之,我希望手动让一个变量强行变为将亡值,在语法上可以做到吗?答案是既可以也不可以。
我们先说可以的部分,直接将变量强制类型转换为右值引用就可以让编译通过了,而且不会发生额外的拷贝构造
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| void ProcessBuf(Buffer &&buf) { for (int i = 0; i < buf.size(); i++) { buf.at(i) = 2 * i; }
for (int i = 0; i < buf.size(); i++) { std::cout << buf.at(i) << " "; } std::cout << "\n"; }
void test2() { Buffer a{5}; a.at(0) = 100;
ProcessBuf(static_cast<Buffer &&>(a));
std::cout << "a should not be used again\n"; }
|
此时我们的程序可以正常编译执行,运行结果如下
1 2 3 4
| call constructor // 在test2函数中,构造局部变量a 0 2 4 6 8 a should not be used again call destructor // test2函数调用结束,局部变量a自动析构
|
我们再说不可以的部分,上述操作只是欺骗编译器的一种文字游戏而已:
- 局部变量
a
看起来被右值引用“夺舍”了,但是我们实际上仍然可以使用它,只是不应该使用它,否则行为未定义;
- 局部变量
a
的析构时机没有被影响,仍然是在test2
函数调用后才被自动析构,右值引用buf
的生命周期与其并不一致。
std::move
上面使用强制类型转换的语法不太优雅,C++直接提供了std::move
函数来简化我们的使用,下面是MSVC的源码
1 2 3 4
| template <class T> constexpr remove_reference_t<T>&& move(T&& Arg) noexcept { return static_cast<remove_reference_t<T>&&>(Arg); }
|
std::move
就是帮我们将普通类型强制类型转换为右值引用类型,仅此而已,当然了为了实现这个功能,代码中还用到了一些类型转换工具和万能引用,对于万能引用的讨论见后面的笔记。
在前面的例子中,使用std::move
是完全一样的效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| void ProcessBuf(Buffer &&buf) { for (int i = 0; i < buf.size(); i++) { buf.at(i) = 2 * i; }
for (int i = 0; i < buf.size(); i++) { std::cout << buf.at(i) << " "; } std::cout << "\n"; }
void test2() { Buffer a{5}; a.at(0) = 100;
ProcessBuf(std::move(a));
std::cout << "a should not be used again\n"; }
|
std::move
所体现的就是移动语义:
- 将一个对象手动变成将亡对象,主动交出控制权,使得右值引用可以绑定并接管它;(实际上并没有影响它的生命周期)
- 被移动的对象不应该被再次使用,除非对变量重新赋值,否则行为未定义。
被移动之后的对象处于一个非常特殊的状态,此时只有对它赋予新值和析构的操作是合法的,其它操作全都是未定义行为!在赋值之后又会恢复到正常状态。
std::move
只是编译期的处理,不会在运行期引入任何额外成本。
移动构造和移动赋值
有了右值引用和移动语义之后,我们还必须明确对自定义类型的下面两个行为的语义:
- 通过自定义类型的右值引用(它接管着某个将亡值对象的“遗体”)如何构造正常的对象,
- 通过自定义类型的右值引用,如何赋值给正常对象。
为了实现这两个功能,我们需要为自定义类型提供移动构造函数/移动赋值运算,它们和拷贝构造函数/赋值运算具有对应关系:
- 前者负责支持“移动”语义,后者负责支持“拷贝”语义;
- 对于管理内存资源的自定义类型,前者的行为通常是“浅复制”,后者的行为通常是“深复制”,这代表前者通常不会遇到内存分配失败等意外情况;
- 赋值和移动赋值都需求考虑自己给自己赋值的特殊情况。
延续上一篇的例子,下面是移动构造和移动赋值的使用示例
1 2 3 4 5 6 7 8 9 10
| void Demo() { Buffer buf1{16};
Buffer buf2(std::move(buf1));
Buffer buf3{8}; buf3 = std::move(buf2); }
|
自定义类型的这四个函数通常具有如下形式
1 2 3 4 5
| Buffer(const Buffer &ob); Buffer &operator=(const Buffer &ob);
Buffer(Buffer &&ob); Buffer &operator=(Buffer &&ob);
|
为了支持移动语义,我们提供的这两个函数需要满足如下要求:
- 移动会让当前对象接管所有的资源,并且保证被移动对象的析构操作是合法但无害的:
- 合法是指不会产生任何错误或异常,通常需要将管理资源的指针置空
- 无害是指不会对当前对象产生任何影响
- 虽然语法上允许移动操作抛异常,但是考虑到性能因素,建议不要在其中抛出任何异常,并且明确标记
noexcept
Buffer
类型的移动构造和移动赋值的具体实现如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| Buffer::Buffer(Buffer &&ob) noexcept : m_buf_size(ob.m_buf_size), m_buf(ob.m_buf) { PRINT_INFO("call move constructor");
ob.m_buf_size = 0; ob.m_buf = nullptr; }
Buffer::Buffer &operator=(Buffer &&ob) noexcept { PRINT_INFO("call move assignment operator"); if (this == &ob) { return *this; }
deallocate(); m_buf_size = ob.m_buf_size; m_buf = ob.m_buf;
ob.m_buf_size = 0; ob.m_buf = nullptr;
return *this; }
|
作为对比,下面是拷贝构造和赋值的具体实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| Buffer::Buffer(const Buffer &ob) { PRINT_INFO("call copy constructor\n"); allocate(ob.m_buf_size); memcpy(m_buf, ob.m_buf, sizeof(int) * ob.m_buf_size); }
Buffer::Buffer &operator=(const Buffer &ob) { PRINT_INFO("call copy operator\n"); if (this == &ob) { return *this; }
deallocate(); allocate(ob.m_buf_size); memcpy(m_buf, ob.m_buf, sizeof(int) * ob.m_buf_size);
return *this; }
|
对于自定义类型,编译器可能会为我们自动生成相应的移动构造和移动赋值运算,具体细节这里不作讨论。
为了安全考虑,对于和Buffer
类似的资源管理类,最好自行提供所有的特殊方法:构造+析构,拷贝构造+赋值,移动构造+移动赋值。