移动语义

An assignment will transfer ownership between variables:

fn main() {
    let s1: String = String::from("Hello!");
    let s2: String = s1;
    println!("s2: {s2}");
    // println!("s1: {s1}");
}
  • s1 赋值给 s2,即转移了所有权。
  • When s1 goes out of scope, nothing happens: it does not own anything.
  • s2 离开作用域时,字符串数据被释放。

移动到 s2 中之前:

StackHeaps1ptrHello!len6capacity6

移动到 s2 中之后:

StackHeaps1ptrHello!len6capacity6s2ptrlen6capacity6(inaccessible)

你将值传递给函数时,该值会被赋给函数 参数。这就转移了所有权:

fn say_hello(name: String) {
    println!("Hello {name}")
}

fn main() {
    let name = String::from("Alice");
    say_hello(name);
    // say_hello(name);
}
This slide should take about 5 minutes.
  • 指出这与 C++ 中的默认值相反。除非你使用 std::move(并已定义 move 构造函数!),否则 C++ 中的默认值是按值复制的。

  • 只有所有权发生了转移。是否会生成任何机器码来操控数据本身是一个优化方面的问题,系统会主动优化此类副本。

  • 简单的值(例如整数)可以标记为“Copy”(请看后续幻灯片)。

  • 在 Rust 中,克隆是显式的(通过使用 clone)。

say_hello 示例中:

  • 首次调用 say_hello 时,main 便放弃了 name 的所有权。此后,main 中不能再使用 name
  • say_hello 函数结束时,系统会释放为 name 分配的堆内存。
  • 如果 mainname 作为引用 (&name) 传递过去,且 say_hello 接受作为参数的引用,则可保留所有权。
  • 此外,main 也可以在首次调用时传递 name 的克隆 (name.clone())。
  • 相较于 C++,Rust 通过将移动语义设为默认值,并强制程序员进行显式克隆,更难以无意中创建副本。

探索更多

Defensive Copies in Modern C++

现代 C++ 以不同的方式解决此问题:

std::string s1 = "Cpp";
std::string s2 = s1;  // Duplicate the data in s1.
  • s1 中的堆数据被复制,s2 获得自己的独立副本。
  • s1s2 离开作用域时,它们会各自释放自己的内存。

复制-赋值之前:

StackHeaps1ptrCpplen3capacity3

复制-赋值之后:

StackHeaps1ptrCpplen3capacity3s2ptrCpplen3capacity3

关键点:

  • C++ 做出了与 Rust 略有不同的选择。由于“=”会复制数据,因此必须克隆字符串数据。否则,当任一字符串超出范围时,便会出现二次释放。

  • C++ 还包含“std::move”,它用于指示何时可以移动某个值。如果示例为“s2 = std::move(s1)”,则不会发生堆分配。移动后,“s1”将处于有效但未指定的状态。与 Rust 不同,程序员可以继续使用“s1”。

  • 与 Rust 不同,使用 C++ 时,“=”可以运行任意代码,具体取决于要复制或移动的类型。