跳转至

移动语义

英文: Move Semantics.
标准: C++11.

移动语义是 C++11 引入的特性, 旨在减少不必要的拷贝.
与移动语义一起被引入的概念还有右值引用 (rvalue references) 和移动构造函数 (move constructor), 但自上而下的理解该概念的方法是了解何为移动语义, 而非何为右值引用. 右值引用只是 C++ 用来实现移动语义的具体实现方法.

为什么需要移动语义?

以下面的字符串类型为例:

class String {
public:
    String(const char* str) {
        buf_ = new char[strlen(str) + 1];
        strcpy(buf_, str);
    }

    // 拷贝构造函数 (深拷贝)
    String(const String& other) : String(other.buf_) {}

    virtual ~String() { delete[] buf_; }

private:
    char* buf_;
};

由于 C++ 默认的拷贝类型为浅拷贝, 因此上面例子为手动实现了进行深拷贝的拷贝函数.
可以看出, 在进行深拷贝时, 需要为字符串分配新的内存, 并将原字符串的内容拷贝到新分配的内存中. 这意味着拷贝 String 有一定的开销.

如果要将 String 放入容器中, 比如 std::vector<String>:

std::vector<String> strings;
{
    String str("hello");
    // 调用 `push_back(const T&)` 和 `String(const String&)`
    strings.push_back(str);
}

我们将变量 str 存入容器中, 以便后续通过容器访问他们. str 变量本身在这之后就不需要使用了.

移动语义

在 C++11 之前, 只有引用传递和值传递两种方式:

  • 值传递: 在传参时就会进行深拷贝.
  • 引用传递: 由于容器保存的是 String, 而非 &String, 所以 push_back(const T&) 函数内部依然需要进行深拷贝.

在 C++11 之后, 引入了移动语义, 包括所谓的右值引用和移动构造函数:

class String {
public:
    // ... SKIP ...

    // 参数类型为右值引用
    String(String&& other) noexcept : buf_(other.buf_) {
        // 防止双重释放
        other.buf_ = nullptr;
    }

    // ... SKIP ...
};

上面代码在原本 String 的基础上添加了移动构造函数, 其参数是一个右值引用.
其中 other 是被移动的变量, 移动后便不应该再被使用. 因此即使其行为与浅拷贝相似, 但依然合法.

其中新的 String 直接接管了旧 Stringbuf_, 没有进行任何的动态内存分配和字符串拷贝操作.

为了调用移动构造函数, 避免不必要的拷贝, 需要先通过 std::move() 函数将 str 从左值转为右值引用. 这样就会通过函数重载, 调用接受右值引用参数的移动构造函数 (即 push_back(T&&)).
其中 std::move() 可以简单的视作 static_cast<T&&>(t), 即将 t 转为右值引用.

使用了移动语义的代码如下:

std::vector<String> strings;
{
    String str("hello");
    // 调用 `push_back(T&& value)` 和 `String(String&& other)`
    strings.push_back(std::move(str));
    // `str` 已经被移动, 不应该继续使用.
    // 生命周期结束后将自动调用析构函数,
    // 由于移动后 `str.buf_` 被赋值为 `nullptr`, 所以后续可以安全的执行 `delete buf_`
}

原位构造 (in-place construction)

针对上述问题, C++11 还给出了一种解决方案, 虽然不能完全替代移动语义, 但是在部分情况下, 甚至比使用移动语义更高效, 因为无需调用移动构造函数.
就是 std::vector<T,Allocator>::emplace_back() 这类函数, 它会直接在容器中构造元素, 无需进行拷贝或移动操作.

std::vector<String> strings;
strings.emplace_back("hello");

不过这已经超出了移动语义的讨论范围, 因此这里不再赘述.

评论