C++ 值类别和移动语义

最后更新: 2021-01-25 02:14:28 +0800 +0800
tags: C++ | 语言
原文以英文撰写。您正在阅读AI翻译版本。

移动语义是重载选择和由类型定义的状态转移,而不是自动转移所有权。应先确定表达式的值类别,再选择描述所有权和生命周期的参数类型。

表达式类别 #

每个非 void 表达式恰好属于以下类别之一:

  • 左值标识一个对象、函数或位域。
  • 亡值标识一个资源可以重用的对象。
  • 纯右值计算一个值或初始化一个对象。

左值和亡值是泛左值;纯右值和亡值是右值。即使命名变量的类型为 T&&,它仍然是左值表达式。

cpp
int& counter();
int&& cache_slot();

counter() = 42;              // The call is an lvalue.
int&& slot = cache_slot();
consume(slot);               // `slot` is an lvalue expression.
consume(std::move(slot));    // Casts it to an xvalue.

返回 T 的调用是纯右值,返回 T& 的调用是左值,返回 T&& 的调用是亡值。std::move 执行亡值转换;它既不移动数据,也不结束源对象的生命周期。

移动构造是重载选择 #

引用绑定提供了相关区别:

  • T& 绑定兼容的左值。
  • const T& 绑定左值、亡值和纯右值。
  • T&& 绑定亡值和纯右值。
cpp
std::string source = "payload";
std::string destination = std::move(source); // Selects a move constructor when viable.

选定的构造函数定义转移行为。它可能转移一次分配、复制小型表示,或产生与复制无法区分的效果。按值返回局部变量时通常应使用 return local;:复制省略可能直接构造结果;不适用省略时,可行的局部变量可以在重载选择中被视为亡值。return std::move(local); 可能抑制具名返回值优化。

转发引用保留调用方的类别 #

推导模板参数中的 T&& 是转发引用。对于左值实参,T 推导为 U&;对于右值,T 推导为 U。引用折叠会将 U& && 化简为 U&,因此 std::forward<T> 可以复现原始类别。

cpp
template<class T>
void relay(T&& value) {
    consume(std::forward<T>(value));
}

std::move(value) 会将两种情况都转换为亡值,并可能意外地从左值调用方移动。未使用推导的 T&&——例如模板类中已经固定 T 的成员——是普通右值引用,而不是转发引用。

参数设计从所有权开始 #

使用描述被调用方契约的参数形式:

形式契约
const T&在调用期间借用,仅用于读取。
T&在调用期间借用并修改。
T获取独立值;调用方将其复制或移动进去。
T&&要求一个被调用方可能消耗的右值;不表示所有权。
std::unique_ptr<T>转移独占所有权。
std::shared_ptr<T>当被调用方保留一份所有权时共享所有权。
T*借用,并由 API 明确可空性。

存储独立值的构造函数可以按值接收参数,然后从参数移动:

cpp
class Message {
public:
    explicit Message(std::string text) : text_{std::move(text)} {}
private:
    std::string text_;
};

左值调用方会将其复制到参数中;右值调用方可以将其移动进去。查询操作或被调用方仅借用对象时,不适合按值传递。

不要使用 FILE*&& 或其他原始指针右值引用来表示所有权。移动原始指针只是复制地址;两个名称仍指向同一资源。当调用转移销毁责任时,应使用带所有权的包装器,例如 std::unique_ptr<T, Deleter>。只使用智能指针所拥有对象的函数,应根据访问方式和可空性接收 T&、const T&、T* 或 const T*;不需要接收智能指针句柄。

被移动对象仍然存活 #

标准库对象被移动后,除非规范另有说明,其值有效但未指定。销毁、赋值以及前置条件接受该状态的操作都是有效的。不要假设被移动的 std::vector 为空或保留容量。

cpp
std::vector<int> buffer = read_row();
rows.push_back(std::move(buffer));
buffer.clear(); // Valid, but capacity reuse is not guaranteed.

借用不得超过所有者的生命周期。移动拥有所有权的 unique_ptr 不会销毁被指对象,也不会消除所有已存在的借用,但之后销毁或重置新所有者可能使这些借用悬空。移动操作解决表示转移问题;它们不会消除声明生命周期的需要。

评论

在 GitHub 上阅读或评论(发表评论需要登录)