Move Semantics

An assignment will transfer ownership between variables:

  1. fn main() {
  2.     let s1: String = String::from("Hello!");
  3.     let s2: String = s1;
  4.     println!("s2: {s2}");
  5.     // println!("s1: {s1}");
  6. }
  • The assignment of s1 to s2 transfers ownership.
  • When s1 goes out of scope, nothing happens: it does not own anything.
  • When s2 goes out of scope, the string data is freed.

Before move to s2:

19.4. Move Semantics - 图1

After move to s2:

19.4. Move Semantics - 图2

When you pass a value to a function, the value is assigned to the function parameter. This transfers ownership:

  1. fn say_hello(name: String) {
  2.     println!("Hello {name}")
  3. }
  4. fn main() {
  5.     let name = String::from("Alice");
  6.     say_hello(name);
  7.     // say_hello(name);
  8. }

This slide should take about 5 minutes.

  • Mention that this is the opposite of the defaults in C++, which copies by value unless you use std::move (and the move constructor is defined!).

  • It is only the ownership that moves. Whether any machine code is generated to manipulate the data itself is a matter of optimization, and such copies are aggressively optimized away.

  • Simple values (such as integers) can be marked Copy (see later slides).

  • In Rust, clones are explicit (by using clone).

In the say_hello example:

  • With the first call to say_hello, main gives up ownership of name. Afterwards, name cannot be used anymore within main.
  • The heap memory allocated for name will be freed at the end of the say_hello function.
  • main can retain ownership if it passes name as a reference (&name) and if say_hello accepts a reference as a parameter.
  • Alternatively, main can pass a clone of name in the first call (name.clone()).
  • Rust makes it harder than C++ to inadvertently create copies by making move semantics the default, and by forcing programmers to make clones explicit.

More to Explore

Defensive Copies in Modern C++

Modern C++ solves this differently:

  1. std::string s1 = "Cpp";
  2. std::string s2 = s1; // Duplicate the data in s1.
  • The heap data from s1 is duplicated and s2 gets its own independent copy.
  • When s1 and s2 go out of scope, they each free their own memory.

Before copy-assignment:

19.4. Move Semantics - 图3

After copy-assignment:

19.4. Move Semantics - 图4

Key points:

  • C++ has made a slightly different choice than Rust. Because = copies data, the string data has to be cloned. Otherwise we would get a double-free when either string goes out of scope.

  • C++ also has std::move, which is used to indicate when a value may be moved from. If the example had been s2 = std::move(s1), no heap allocation would take place. After the move, s1 would be in a valid but unspecified state. Unlike Rust, the programmer is allowed to keep using s1.

  • Unlike Rust, = in C++ can run arbitrary code as determined by the type which is being copied or moved.