2.4 移动语义
04-移动语义 — Comprehensive Rust
3 分钟阅读
译文 · 基于 Comprehensive Rust
原文链接: https://google.github.io/comprehensive-rust/memory-management/move.html
2.4 移动语义
赋值会在变量之间转移_所有权_(ownership):
| |
- 把
s1赋给s2会转移所有权。 - 当
s1离开作用域时什么也不发生:它已不再拥有任何东西。 - 当
s2离开作用域时,字符串数据被释放。
移动到 s2 之前:
Stack Heap
.- - - - - - - - - - - - - -. .- - - - - - - - - - - - - - - - - - -.
: : : :
: s1 : : :
: +-----------+-------+ : : +----+----+----+----+----+----+ :
: | ptr | o---+---+-----+-->| H | e | l | l | o | ! | :
: | len | 6 | : : +----+----+----+----+----+----+ :
: | capacity | 6 | : : :
: +-----------+-------+ : : :
: : `- - - - - - - - - - - - - - - - - - -'
: :
`- - - - - - - - - - - - - -'
移动到 s2 之后:
Stack Heap
.- - - - - - - - - - - - - -. .- - - - - - - - - - - - - - - - - - -.
: : : :
: s1 "(inaccessible)" : : :
: +-----------+-------+ : : +----+----+----+----+----+----+ :
: | ptr | o---+---+--+--+-->| H | e | l | l | o | ! | :
: | len | 6 | : | : +----+----+----+----+----+----+ :
: | capacity | 6 | : | : :
: +-----------+-------+ : | : :
: : | `- - - - - - - - - - - - - - - - - - -'
: s2 : |
: +-----------+-------+ : |
: | ptr | o---+---+--'
: | len | 6 | :
: | capacity | 6 | :
: +-----------+-------+ :
: :
`- - - - - - - - - - - - - -'
把值传给函数时,该值会赋给函数参数,从而转移所有权:
| |
可以指出:这与 C++ 的默认相反——C++ 默认按值拷贝,除非使用
std::move(且定义了移动构造函数)。移动的只是所有权。是否生成机器码去搬动数据本身属于优化问题,这类拷贝会被积极优化掉。
简单值(如整数)可以标记为
Copy(见后续幻灯片)。在 Rust 中,克隆是显式的(使用
clone)。在
say_hello示例中:
- 第一次调用
say_hello时,main放弃了name的所有权。之后在main中不能再使用name。- 为
name分配的堆内存会在say_hello函数结束时释放。- 若
main传入name的引用(&name),且say_hello接受引用参数,则main可保留所有权。- 或者,
main可在第一次调用时传入name的克隆(name.clone())。- Rust 把移动语义设为默认,并强制程序员显式克隆,从而比 C++ 更难无意中产生拷贝。
延伸阅读
现代 C++ 中的防御性拷贝
现代 C++ 用不同方式解决这一问题:
1 2std::string s1 = "Cpp"; std::string s2 = s1; // Duplicate the data in s1.
s1的堆数据被复制,s2得到独立的一份拷贝。- 当
s1与s2离开作用域时,各自释放自己的内存。拷贝赋值之前:
Stack Heap .- - - - - - - - - - - - - -. .- - - - - - - - - - - -. : : : : : s1 : : : : +-----------+-------+ : : +----+----+----+ : : | ptr | o---+---+--+--+-->| C | p | p | : : | len | 3 | : : +----+----+----+ : : | capacity | 3 | : : : : +-----------+-------+ : : : : : `- - - - - - - - - - - -' `- - - - - - - - - - - - - -'拷贝赋值之后:
Stack Heap .- - - - - - - - - - - - - -. .- - - - - - - - - - - -. : : : : : s1 : : : : +-----------+-------+ : : +----+----+----+ : : | ptr | o---+---+--+--+-->| C | p | p | : : | len | 3 | : : +----+----+----+ : : | capacity | 3 | : : : : +-----------+-------+ : : : : : : : : s2 : : : : +-----------+-------+ : : +----+----+----+ : : | ptr | o---+---+-----+-->| C | p | p | : : | len | 3 | : : +----+----+----+ : : | capacity | 3 | : : : : +-----------+-------+ : : : : : `- - - - - - - - - - - -' `- - - - - - - - - - - - - -'要点:
C++ 与 Rust 做了略有不同的选择。因为
=会拷贝数据,字符串数据必须被克隆,否则任一字符串离开作用域时会出现 double-free。C++ 也有
std::move,用于标明何时可以从一个值移动。若写成s2 = std::move(s1),则不会发生堆分配。移动后s1处于有效但未指定的状态。与 Rust 不同,程序员仍允许继续使用s1。与 Rust 不同,C++ 中的
=可以根据被拷贝或移动的类型运行任意代码。