C++ 中的移动语义 Move Semantics

有时候拷贝对象的成本太高, 会显著影响 C++ 代码的性能.

C++11 之前的世界

之前的 C++ 版本中, 鼓励以复制的方式来构造对象, 比如:

std::string s1 = "C++";
std::string s2 = s1;

上面的代码中, 字符串 s2 会复制一份与 s1 相同的堆内存, 这之后它们两个不再有任何关联. 就像下图展示的那样:

cpp copy string

下面是一个更复杂的例子:

#include <cstdio>
#include <cassert>

#include <string>

class Person {
public:
  explicit Person(const std::string& name) noexcept : name_(name) {
    printf("Person(const string&) %s\n", name_.c_str());
  }

  Person(const Person& other) : name_(other.name_) {
    printf("Person(const Person&)\n");
  }

  Person(Person&& other) noexcept = delete;

  ~Person() = default;

  Person& operator=(const Person& other) {
    if (this == &other) {
      return *this;
    }
    this->name_ = other.name_;
    return *this;
  }

  Person& operator=(Person&& other) noexcept = delete;

  const std::string& name() const { return name_; }

private:
  std::string name_;
};


int main(int argc, char** argv) {
  (void)argc;
  (void)argv;

  std::string name = "Julia";
  Person p2(name);
  assert(!name.empty());
  name.clear();

  printf("creating p3\n");
  Person p3("Julia");

  // 使用 copy constructor
  printf("creating p4:\n");
  Person p4(p3);

  return 0;
}

为了简化图例, 本文忽略了 std::string 相关的 SSO (short string optimization), 但这对本文的核心没有影响. 更多关于 SSO 的信息可以参考本文结尾的链接.

C++11 引入 移动语义 Move semantics

移动语义依赖三个基础:

  • move constructor
  • move assignment operator
  • std::move()

这是对 C++ 过渡封装的补救.

  • 一个右值引用参数
  • 转移所有权
  • 原有的对象仍然保持有效状态
  • 它是浅拷贝

C++ 中的右值引用 Rvalue Reference

std::move() 将左值 (lvalue) 对象转换成对应的右值引用(rvalue reference).

什么是左值 lvalue?

  • 可以出现在赋值表达式的左侧
  • 有名字
  • 有内存地址

什么是右值 rvalue?

  • 除了不是 lvalue 的, 都是 rvalue
  • 临时对象
  • 字面量常量 literal constants, 比如 "Hello, C++"
  • 函数返回值 (不是左值引用 lvalue reference)
std::string s1 = "C++";
std::string s2 = std::move(s1);

上面的代码片段中, 字符串 s2s1 原有内存的浅拷贝; 而 s1 里面的堆内存被重新设置了, 并且其字符串长度 size == 0.

cpp move string

下面是一个更复杂的例子, Person 类额外实现了

  • move constructor
  • move assignment operator

在创建对象时可以使用它们进行浅拷贝, 以提高程序的速度.

#include <cstdio>
#include <cassert>

#include <string>

class Person {
public:
  explicit Person(std::string&& name) noexcept : name_(std::move(name)) {
    printf("Person(string&&) %s\n", name_.c_str());
  }

  Person(const Person& other) : name_(other.name_) {
    printf("Person(const Person&)\n");
  }

  Person(Person&& other) noexcept : name_(std::move(other.name_)) {
    printf("Person(Person&&)\n");
  }

  ~Person() = default;

  Person& operator=(const Person& other) {
    if (this == &other) {
      return *this;
    }
    this->name_ = other.name_;
    return *this;
  }

  Person& operator=(Person&& other) noexcept {
    if (this == &other) {
      return *this;
    }
    this->name_ = std::move(other.name_);
    return *this;
  }

  const std::string& name() const { return name_; }

private:
  std::string name_;
};

int main(int argc, char** argv) {
  (void)argc;
  (void)argv;

  std::string name = "Julia";
  Person p2(std::move(name));
  assert(name.empty());

  printf("creating p3\n");
  Person p3("Julia");

  // 使用 copy constructor
  printf("creating p4:\n");
  Person p4(p3);

  // 使用 move constructor
  printf("creating p5:\n");
  Person p5(std::move(p4));

  return 0;
}

参考