首页 > 编程笔记 > C++笔记 阅读:1

C++移动语义详解(附带实例)

移动语义是现代 C++ 提升性能的关键特性。它们支持移动而不是复制那些复制成本较高的资源或对象。然而,它要求类实现移动构造函数和赋值操作符。在某些情景下,编译器会提供它们,但实际上,通常你需要自己显式实现它们。

本节中,我们将展示如何基于以下 Buffer 类实现移动构造函数和赋值操作符:
class Buffer
{
    unsigned char* ptr;
    size_t length;
public:
    Buffer(): ptr(nullptr), length(0)
    {}

    explicit Buffer(size_t const size):
        ptr(new unsigned char[size] {0}), length(size)
    {}

    ~Buffer()
    {
        delete[] ptr;
    }

    Buffer(Buffer const& other):
        ptr(new unsigned char[other.length]),
        length(other.length)
    {
        std::copy(other.ptr, other.ptr + other.length, ptr);
    }

    Buffer& operator=(Buffer const& other)
    {
        if (this != &other)
        {
            delete[] ptr;

            ptr = new unsigned char[other.length];
            length = other.length;

            std::copy(other.ptr, other.ptr + other.length, ptr);
        }
        return *this;
    }

    size_t size() const { return length; }
    unsigned char* data() const { return ptr; }
};

C++移动语义的具体实现

为了实现类的移动构造函数,如下做:
1) 写一个接受此类类型右值引用的构造函数:
Buffer(Buffer&& other)
{
}

2) 将右值引用的所有数据成员赋值到当前对象。这可以在如下构造函数主体中,或在初始化列表中(更好的方式)完成:
ptr = other.ptr;
length = other.length;

3) 将右值引用的所有数据成员赋予默认值:
other.ptr = nullptr;
other.length = 0;
将它们放在一起,Buffer 类的移动构造函数如下:
Buffer(Buffer&& other)
{
    ptr = other.ptr;
    length = other.length;
    other.ptr = nullptr;
    other.length = 0;
}

为了实现类的移动赋值操作,请执行如下步骤:
1) 写一个接受此类类型右值引用的赋值操作符并返回引用:
Buffer& operator=(Buffer&& other)
{
}

2) 检查右值引用没有指向this同一对象,如果它们不同,则执行步骤 3~5:
if (this != &other)
{
}

3) 从当前对象处理所有资源(如内存、句柄等):
delete[] ptr;

4) 将右值引用的所有数据成员赋值给当前对象:
ptr = other.ptr;
length = other.length;

5) 将右值引用的所有数据成员赋予默认值:
other.ptr = nullptr;
other.length = 0;

6) 无论步骤 3)~步骤 5) 执行与否,返回当前对象的引用:
return *this;

将它们放在一起,Buffer 类的移动赋值操作符如下:
Buffer& operator=(Buffer&& other)
{
    if (this != &other)
    {
        delete[] ptr;
        ptr = other.ptr;
        length = other.length;
        other.ptr = nullptr;
        other.length = 0;
    }
    return *this;
}

深度剖析C++移动语义

除非用户定义的复制构造函数、移动构造函数、复制赋值操作符、移动赋值操作符或析构函数已经存在,否则编译器默认提供移动构造函数和移动赋值操作符。当编译器提供它们时,它们智能地移动成员。

移动构造函数将递归调用类数据成员的移动构造函数。类似地,移动赋值操作符递归调用类数据成员的移动赋值操作符。

移动对于太大以至于不能复制的对象(如字符串或容器)或不能被复制的对象(如 unique_ptr 智能指针)有性能提升。不是所有类都应该实现复制和移动语义。有些类只应该可移动,另一些类可同时复制和移动。另外,类能复制却不能移动是没有意义的,尽管技术上可以这么做。

不是所有类型都能从移动语义上获利。对于内置类型(如 bool、int 或 double)、数组或 POD,移动实际上是复制操作。另外,移动语义对于右值(临时对象)有性能提升。右值是没有名称的对象,在表达式运算期间临时存在,在下一分号则销毁:
T a;
T b = a;
T c = a + b;
在前面示例中,a、b 和 c 是左值,它们是有名称的对象,在生命周期内,名称可指向此对象。另外,当你运算表达式 a+b,编译器创建临时对象(此例中赋值给 c),然后销毁(当遇到分号时)。这些临时对象被称为右值,因为它们通常出现在赋值表达式的右边。C++11 中,我们可通过右值引用 && 来引用这些对象。

在右值语境下,移动语义很重要。这是因为它允许你从将被销毁的临时对象中获取资源的所有权,当移动操作完成后,用户不需要再使用它。

另外,左值不能被移动,它们只能被复制。这是因为它们在移动操作后,还需要被访问,用户期望对象在相同的状态。例如,在前一示例中,表达式 b=a 将 a 赋值给 b。

在此操作完成后,左值对象 a 还能被用户使用且应该处于之前同样的状态。另外,a+b 的结果是临时的,它的数据可以被安全地移动到 c。

移动构造函数区别于复制构造函数,因为它接收类类型T的右值引用(T&&),与之相反,复制构造函数T则接受左值引用(T const &)。类似地,移动赋值接受右值引用 T&operator=(T&&),与之相反,复制赋值接受左值引用 T& operator=(T const &),即两者都返回 T& 类引用。编译器基于参数类型、右值或左值,以选择合适的构造函数或赋值操作符。

当移动构造函数或移动赋值操作符存在,右值被自动移动。左值也可移动,但需要显式的静态转换为右值引用。这可通过 std::move() 函数完成,它执行了 static_cast<T&&>:
std::vector<Buffer> c;
c.push_back(Buffer(100));  // move

Buffer b(200);            // copy
c.push_back(b);
c.push_back(std::move(b)); // move
对象移动后,它必须保持有效状态。然而,并不要求这个状态是什么。为了一致性,你应该将所有成员设置为默认值(数字类型为 0,指针为 nullptr,布尔类型为 false 等)。

以下示例展示了 Buffer 对象不同的构造和赋值方式:
Buffer b1;                // default constructor
Buffer b2(100);           // explicit constructor
Buffer b3(b2);            // copy constructor
b1 = b3;                  // assignment operator
Buffer b4(std::move(b1));  // move constructor
b3 = std::move(b4);       // move assignment
在对象 b1、b2、b3 和 b4 的创建或赋值中用到的构造函数和赋值操作符在每行的注释中可见。

如 Buffer 示例所见,实现移动构造函数和移动赋值操作符的代码很相似(移动构造函数代码在移动赋值操作符中可见)。这可通过在移动构造函数中调用移动赋值操作符来避免:
Buffer(Buffer&& other) : ptr(nullptr), length(0)
{
    *this = std::move(other);
}
在此示例中,有两点必须注意:

相关文章