C++ / a working model

48 / 80   ·   C++11   ·   约 9 分钟

值类别与 std::move:转换不等于搬运

先记住这句话

左值、将亡值与纯右值描述表达式,而不是变量永久拥有的标签。std::move 只把表达式转换为可参与移动重载的形式;实际是否转移资源、源对象留下什么状态,取决于被调用类型的契约。

本篇内容
  1. 用表达式分析值类别
  2. 移动由接收方实施
  3. 什么时候不要主动 move
  4. 运行示例
  5. 动手练习

用表达式分析值类别

C++11 将表达式细分为左值 lvalue、将亡值 xvalue 和纯右值 prvalue。左值与将亡值合称 glvalue,强调对象身份;纯右值与将亡值合称 rvalue,可参与右值引用绑定。不要仅用“在等号哪一边”判断,因为字面量、函数调用与引用表达式都有自己的规则。

一个有名字的变量表达式通常是左值,即使变量声明类型为 T&&。例子中 r 是右值引用变量,但把 r 传给重载函数时选择左值版本;再写 std::move(r) 才得到将亡值。这区分了声明类型与使用表达式的类别。

移动由接收方实施

std::move(x) 本质上是保留底层 cv 属性的转换,不执行资源转移,也不会独立改变 x。将结果交给构造函数或赋值运算符后,重载选择才可能调用移动操作。对于拥有独占资源的类型,这一步可以把资源管理权交给目标。

示例用 unique_ptr 验证转移后的明确契约:目标仍指向原来的整数,源变为空指针。不能把这条结论推广到所有容器;很多标准库类型只保证移动后有效但值未指定。此时可析构、赋新值,以及执行满足前置条件的操作,但不应断言原内容仍在或必然为空。

什么时候不要主动 move

对 const 对象使用 std::move 通常得到 const 右值引用,而常见移动构造函数需要可修改的 T&&,于是仍可能选中复制构造。应从对象是否允许交出资源出发设计 const,而不是在调用处堆转换试图强迫移动。

返回局部值时通常直接 return local,让编译器利用命名返回值优化或隐式移动规则。写 return std::move(local) 可能阻止 NRVO。C++17 的部分纯右值返回路径具有保证的消除语义,但不能据此宣称所有返回都会执行移动,或者所有移动都比复制便宜。

容易答错的地方

  • std::move 不会解除 const;若接收方没有匹配的移动重载,转换后的表达式仍可能触发复制。
  • 移动后读取源对象的旧业务值不能当作通用契约;尤其不能把 unique_ptr 的置空保证推广到 string。

运行一个例子

最低标准 C++11 · 完整程序 · 下载 .cpp

#include <cassert>
#include <iostream>
#include <memory>
#include <utility>

int category(int&) { return 1; }
int category(int&&) { return 2; }

int main() {
    int n = 5;
    int&& r = std::move(n);
    assert(category(r) == 1);
    assert(category(std::move(r)) == 2);
    assert(n == 5);
    std::unique_ptr<int> source(new int(42));
    int* original = source.get();
    std::unique_ptr<int> target = std::move(source);
    assert(!source);
    assert(target.get() == original && *target == 42);
    std::cout << category(r) << ' ' << category(std::move(r)) << ' ' << *target << '\n';
}

在本地编译

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread modern-move.cpp -o example && ./example

预期结果

1 2 42

CHECK YOUR UNDERSTANDING

合上答案,试着解释。

执行 int&& r = std::move(n) 后,给 r 赋值 8 会不会改变 n?这一步有没有构造新的 int?

查看参考答案

会改变 n,因为 r 直接引用 n,并没有构造第二个 int。std::move(n) 改变的是表达式类别,不是存储身份。只有接收这个表达式的操作决定是否创建目标对象以及如何转移其内容。

继续查证

标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。

回到目录