目录
1. 移动语义
移动语义是 C++ 语言的一组语义规则和工具。它旨在移动生命周期已到期的对象,而不是复制它们。数据从一个对象传输到另一个对象。在大多数情况下,数据传输不会将数据物理地移动到内存中。这有助于避免昂贵的复制操作。
移动语义是在 C++11 标准中引入的。为此,添加了右值引用、移动构造函数和移动赋值运算符。此外,标准模板库 (STL) 中也添加了一些函数来支持移动语义。例如,std::move 和 std::forward。
移动构造函数允许将右值对象所拥有的资源移动到左值中,而无需复制。
2. 使用移动语义
假设你需要编写一个 Swap 模板函数,它接受两个相同类型的对象并交换它们。该函数可以按如下方式实现:
template <typename T>
void Swap(T &lhs, T &rhs)
{
T t = lhs;
lhs = rhs;
rhs = t;
}
一切看起来都很棒——该函数可以处理两个相同类型的对象并交换它们。然而,这样的实现有一个明显的缺点。让我们看一下以下代码片段:
std::vector<int> arrA(1'000'000, 0);
std::vector<int> arrB(1'000'000, 1);
Swap(arrA, arrB);
创建两个 std::vector<int> 类型的对象。每个对象包含 1,000,000 个元素。然后,Swap 函数交换它们的位置。std::vector 类模板包含一个非平凡的复制构造函数,该构造函数具有以下函数(注:在 C++ 中,“非平凡复制”是指不是由编译器隐式生成或需要对对象内存进行简单按位复制的复制操作(复制构造函数或复制赋值运算符);也就是说,需要用定义构造函数或复制构造函数来完成深复制):
• 对所需数量的元素执行动态内存分配;
• 从传递的 std::vector 中对元素进行深复制。
结果,我们有 3,000,000 个 int 类型对象的副本。如果 std::vector 被非平凡复制类型实例化,情况可能会更糟。
移动语义有助于避免不必要的复制。为此,我们需要将对象转换为右值引用。这样,我们就可以告诉编译器它可以移动该对象。
#include <type_traits>
template <typename T>
void Swap(T &lhs, T &rhs) noexcept
{
using rvalue_ref = typename std::remove_reference<T>::type &&;
T t = static_cast<rvalue_ref>(lhs);
lhs = static_cast<rvalue_ref>(rhs);
rhs = static_cast<rvalue_ref>(t);
}
对于 std::vector,其非平凡构造函数/移动运算符将指针交换到动态内存,从而消除昂贵的内存分配和元素复制。
为了简化移动对象的编码过程,标准库中引入了 std::move 函数。该函数将传递给它的对象强制转换为右值引用:
#include <utility>
template <typename T>
void Swap(T &lhs, T &rhs) noexcept
{
T t = std::move(lhs);
lhs = std::move(rhs);
rhs = std::move(t);
}
说明:move 函数实现的就是将对象转换为移动赋值,其实现如下(以windows平台为例):
_EXPORT_STD template <class _Ty>
_NODISCARD _MSVC_INTRINSIC constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept {
return static_cast<remove_reference_t<_Ty>&&>(_Arg);
}
move做了两件事:用一个结构体记住右值的类型,以及引用类型。
remove_reference:
template <class _Ty>
struct remove_reference<_Ty&&> {
using type = _Ty;
using _Const_thru_ref_type = const _Ty&&;
};
remove_reference_t:
_EXPORT_STD template <class _Ty>
using remove_reference_t = typename remove_reference<_Ty>::type;
3. 编写移动构造函数和移动赋值运算符函数
移动构造函数允许将右值对象所拥有的资源移动到左值中(将右值地址赋给左值地址),而无需复制内容。
下述例子引用自微软文档:
// MemoryBlock.h
#pragma once
#include <iostream>
#include <algorithm>
class MemoryBlock
{
public:
// Simple constructor that initializes the resource.
explicit MemoryBlock(size_t length)
: _length(length)
, _data(new int[length])
{
std::cout << "In MemoryBlock(size_t). length = "
<< _length << "." << std::endl;
}
// Destructor.
~MemoryBlock()
{
std::cout << "In ~MemoryBlock(). length = "
<< _length << ".";
if (_data != nullptr)
{
std::cout << " Deleting resource.";
// Delete the resource.
delete[] _data;
}
std::cout << std::endl;
}
// Copy constructor.
MemoryBlock(const MemoryBlock& other)
: _length(other._length)
, _data(new int[other._length])
{
std::cout << "In MemoryBlock(const MemoryBlock&). length = "
<< other._length << ". Copying resource." << std::endl;
std::copy(other._data, other._data + _length, _data);
}
// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other)
{
std::cout << "In operator=(const MemoryBlock&). length = "
<< other._length << ". Copying resource." << std::endl;
if (this != &other)
{
// Free the existing resource.
delete[] _data;
_length = other._length;
_data = new int[_length];
std::copy(other._data, other._data + _length, _data);
}
return *this;
}
// Retrieves the length of the data resource.
size_t Length() const
{
return _length;
}
MemoryBlock(MemoryBlock&& other)
: _data(nullptr)
, _length(0)
{
this->_data = other._data;
this->_length = other._length;
other._data = nullptr;
other._length = 0;
}
MemoryBlock& operator=(MemoryBlock&& other)
{
if (this != &other) //避免做无用操作
{
// Free the existing resource.
delete[] this->_data;
// Copy the data pointer and its length from the
// source object.
this->_data = other._data;
this->_length = other._length;
// Release the data pointer from the source object so that
// the destructor does not free the memory multiple times.
other._data = nullptr;
other._length = 0;
}
return (*this);
}
private:
size_t _length; // The length of the resource.
int* _data; // The resource.
};
为这个 C++ 类创建移动构造函数的过程:
(1) 定义一个空的构造函数方法,该方法以类类型的右值引用作为其参数,如下例所示:
MemoryBlock(MemoryBlock&& other)
: _data(nullptr)
, _length(0)
{
}
(2) 在移动构造函数中,将源对象的类数据成员赋值给正在构造的对象:
this->_data = other._data;
this->_length = other._length;
(3) 将源对象的数据成员赋值为默认值。这可以防止析构函数多次释放资源(例如内存):
other._data = nullptr;
other._length = 0;
为 C++ 类创建移动赋值运算符步骤如下:
(1) 定义一个空赋值运算符函数,该运算符函数以类类型的右值引用作为参数,并返回该类类型的引用,如下例所示:
MemoryBlock& operator=(MemoryBlock&& other)
{
}
(2) 在移动赋值运算符中,添加一个条件语句,如果您尝试将对象分配给自身,则该语句不执行任何操作。
if (this != &other) //避免做无用操作
{
}
(3) 在条件语句中,释放正在被赋值的对象的所有资源(如内存)。以下示例从正在被赋值的对象中释放 _data 成员:
// Free the existing resource.
delete[] this->_data;
按照第一个过程中的步骤 (2) 和 (3) 将数据成员从源对象传输到正在构造的对象:
// Copy the data pointer and its length from the
// source object.
this->_data = other._data;
this->_length = other._length;
// Release the data pointer from the source object so that
// the destructor does not free the memory multiple times.
other._data = nullptr;
other._length = 0;
(4) 返回对当前对象的引用,如下例所示:
return (*this);
以下示例展示了移动语义如何提升应用程序的性能。该示例向 vector 对象添加两个元素,然后在两个现有元素之间插入一个新元素。vector类使用移动语义,通过移动 vector的元素(而不是复制元素)来高效地执行插入操作。
// rvalue-references-move-semantics.cpp
// compile with: /EHsc
#include "MemoryBlock.h"
#include <vector>
using namespace std;
int main()
{
// Create a vector object and add a few elements to it.
vector<MemoryBlock> v;
v.push_back(MemoryBlock(25));
v.push_back(MemoryBlock(75));
// Insert a new element into the second position of the vector.
v.insert(v.begin() + 1, MemoryBlock(50));
}
程序产生下列输出:
In MemoryBlock(size_t). length = 25.
In MemoryBlock(MemoryBlock&&). length = 25. Moving resource.
In ~MemoryBlock(). length = 0.
In MemoryBlock(size_t). length = 75.
In MemoryBlock(MemoryBlock&&). length = 75. Moving resource.
In MemoryBlock(MemoryBlock&&). length = 25. Moving resource.
In ~MemoryBlock(). length = 0.
In ~MemoryBlock(). length = 0.
In MemoryBlock(size_t). length = 50.
In MemoryBlock(MemoryBlock&&). length = 50. Moving resource.
In MemoryBlock(MemoryBlock&&). length = 25. Moving resource.
In MemoryBlock(MemoryBlock&&). length = 75. Moving resource.
In ~MemoryBlock(). length = 0.
In ~MemoryBlock(). length = 0.
In ~MemoryBlock(). length = 0.
In ~MemoryBlock(). length = 25. Deleting resource.
In ~MemoryBlock(). length = 50. Deleting resource.
In ~MemoryBlock(). length = 75. Deleting resource.
在 Visual Studio 2010 之前,例子产生如下输出:
In MemoryBlock(size_t). length = 25.
In MemoryBlock(const MemoryBlock&). length = 25. Copying resource.
In ~MemoryBlock(). length = 25. Deleting resource.
In MemoryBlock(size_t). length = 75.
In MemoryBlock(const MemoryBlock&). length = 25. Copying resource.
In ~MemoryBlock(). length = 25. Deleting resource.
In MemoryBlock(const MemoryBlock&). length = 75. Copying resource.
In ~MemoryBlock(). length = 75. Deleting resource.
In MemoryBlock(size_t). length = 50.
In MemoryBlock(const MemoryBlock&). length = 50. Copying resource.
In MemoryBlock(const MemoryBlock&). length = 50. Copying resource.
In operator=(const MemoryBlock&). length = 75. Copying resource.
In operator=(const MemoryBlock&). length = 50. Copying resource.
In ~MemoryBlock(). length = 50. Deleting resource.
In ~MemoryBlock(). length = 50. Deleting resource.
In ~MemoryBlock(). length = 25. Deleting resource.
In ~MemoryBlock(). length = 50. Deleting resource.
In ~MemoryBlock(). length = 75. Deleting resource.
此示例的使用移动语义的版本比不使用移动语义的版本更高效,因为它执行的复制、内存分配和内存释放操作更少。
4. 健壮编程
为防止资源泄漏,请务必在移动赋值运算符中释放资源(例如内存、文件句柄和套接字)。
为防止资源被不可恢复地破坏,请在移动赋值运算符中正确处理自赋值。
如果你为类同时提供了移动构造函数和移动赋值运算符,则可以通过编写移动构造函数来调用移动赋值运算符以消除冗余代码。以下示例展示了调用移动赋值运算符的移动构造函数的修订版本:
// Move constructor.
MemoryBlock(MemoryBlock&& other) noexcept
: _data(nullptr)
, _length(0)
{
(*this) = std::move(other); //这会触发调用移动赋值函数
}
注:从移动构造函数可以看出,移动操作只对创建对象的基本结构,不会调用构造函数创建新的对象,对于对象内部的堆内存不会分配新的内存,而只是将右健参数的地址直接赋值给新的结构。但这个新的结构也会占用一定的内存,只是少了动态分配的内存。