C++ 必知必会: 移动语义(Move Semantics)

引言

在现代 C++ 中, Move 语义通过优化资源管理和减少内存复制, 显著提升了程序性能. 它在处理动态内存分配和容器操作等大数据场景中尤为重要. 本文将通过多个实用案例, 深入探讨 Move 语义的核心概念和应用场景.


Move 语义的定义与优点

传统 C++ 中的拷贝语义(Copy Semantics)通常通过构造函数和赋值运算符来复制对象的内容. 然而, 当对象包含动态分配的资源时, 深拷贝会带来高明性能应用. Move 语义通过移动资源的所有权, 避免了这些消耗.

示例: 拷贝和移动操作

std::vector<std::string> coll;
std::string str = "example";

// 拷贝操作
coll.push_back(str); // str内容被拷贝

// 移动操作
coll.push_back(std::move(str)); // str的内容被移动

在上述示例中, std::move将对象转化为右值引用, 使其内容被高效迁移.


应用场景

如果一个对象的值不再被继续使用, 则可以考虑使用 Move. 临时对象是一个特别好的例子.

#include <string>
#include <vector>

std::string GetStr() { return "Hello"; }
int main() {
  std::vector<std::string> vec;
  auto s = GetStr();

  vec.emplace_back(s);             // copy
  vec.emplace_back(s + "World");   // move
  vec.emplace_back(GetStr());      // move
  vec.emplace_back(std::move(s));  // move
  return 0;
}

Move 后的变量

C++标准规定, 被移动对象仍然必须保持在有效但未指定的状态(valid but unspecified state). 这意味着您可以安全地访问被移动对象, 只是不能依赖它的内容.

#include <iostream>
#include <string>
#include <vector>

int main() {
  std::vector<std::string> vec;
  std::string s = "Hello";

  vec.emplace_back(std::move(s));  // move
  std::cout << s << std::endl;     // s依旧可用, 但不确定其值
  s = "world";                     // OK, 可以被重新赋值
  std::cout << s << std::endl;     // OK, s目前有明确的值
  return 0;
}

自定义类中如何支持 Move

一个自定义类通过定义 move 构造函数和 move 赋值运算符来实现对 move 语义的支持.

std::move并不真正移动对象, 而是将对象转化为右值引用(rvalue reference), 为调用相应的移动构造函数或移动赋值运算符提供条件.

#include <algorithm>

class MyString {
 private:
  char* data;
  size_t length;

 public:
  // 移动构造函数
  MyString(MyString&& other) noexcept : data(other.data), length(other.length) {
    other.data = nullptr;  // 防止析构时释放原资源
    other.length = 0;
  }

  // 移动赋值运算符
  MyString& operator=(MyString&& other) noexcept {
    if (this != &other) {
      delete[] data;  // 释放已有资源
      data = other.data;
      length = other.length;
      other.data = nullptr;  // 防止析构时释放原资源
      other.length = 0;
    }
    return *this;
  }

  // 拷贝构造函数
  MyString(const MyString& other)
      : data(new char[other.length]), length(other.length) {
    std::copy(other.data, other.data + other.length, data);
  }

  // 析构函数
  ~MyString() { delete[] data; }
};

Move 语义与noexcept共同使用可确保移动操作的安全性. 例如: 在容器重分配内存时, 如果移动构造函数是noexcept的, 标准库会优先使用 Move 语义, 而非拷贝.


Move 语义的误区

std::move 并不移动数据

它仅将对象标记为右值, 具体移动操作由类的实现进行.

移动后继续使用对象

例如:

std::string s1 = "abc";
auto s2 = std::move(s1);

auto c = s1[0]; // Error

对整型或者布尔类型无效

对于int, float, bool等这些类型的 move 没有意义. 这些基础类型的赋值就是拷贝. 同理, 对于像下面这种内部只含有基础类型的 move 也没有意义, 就是拷贝.

struct POD {
  int a;
  int b;
  bool c;
};

POD a;
POD b = std::move(a); // copy

总结

Move 语义为 C++带来了显著的性能提升, 尤其在需要频繁资源管理的场景中. 理解和正确使用 Move 语义, 是现代 C++ 开发者的一项基本技能. 通过合理设计移动构造函数和移动赋值运算符, 您可以编写出高效且健壮的 C++ 程序.

参考链接

Back to Basics: Move Semantics - Nicolai Josuttis - CppCon 2021 C++ Move Semantics - The Complete Guide