Back to Comprehensive Rust

Move Semantics

src/memory-management/move.md

latest6.4 KB
Original Source
<!-- Copyright 2023 Google LLC SPDX-License-Identifier: CC-BY-4.0 -->

Move Semantics

An assignment will transfer ownership between variables:

rust,editable
# // Copyright 2023 Google LLC
# // SPDX-License-Identifier: Apache-2.0
#
fn main() {
    let s1 = String::from("Hello!");
    let s2 = s1;
    dbg!(s2);
    // dbg!(s1);
}
  • 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:

bob
 Stack                             Heap
.- - - - - - - - - - - - - -.     .- - - - - - - - - - - - - - - - - - -.
:                           :     :                                     :
:    s1                     :     :                                     :
:   +-----------+-------+   :     :   +----+----+----+----+----+----+   :
:   | ptr       |   o---+---+-----+-->| H  | e  | l  | l  | o  | !  |   :
:   | len       |     6 |   :     :   +----+----+----+----+----+----+   :
:   | capacity  |     6 |   :     :                                     :
:   +-----------+-------+   :     :                                     :
:                           :     `- - - - - - - - - - - - - - - - - - -'
:                           :
`- - - - - - - - - - - - - -'

After move to s2:

bob
 Stack                             Heap
.- - - - - - - - - - - - - -.     .- - - - - - - - - - - - - - - - - - -.
:                           :     :                                     :
:    s1 "(inaccessible)"    :     :                                     :
:   +-----------+-------+   :     :   +----+----+----+----+----+----+   :
:   | ptr       |   o---+---+--+--+-->| H  | e  | l  | l  | o  | !  |   :
:   | len       |     6 |   :  |  :   +----+----+----+----+----+----+   :
:   | capacity  |     6 |   :  |  :                                     :
:   +-----------+-------+   :  |  :                                     :
:                           :  |  `- - - - - - - - - - - - - - - - - - -'
:    s2                     :  |
:   +-----------+-------+   :  |
:   | ptr       |   o---+---+--'
:   | len       |     6 |   :
:   | capacity  |     6 |   :
:   +-----------+-------+   :
:                           :
`- - - - - - - - - - - - - -'

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

rust,editable
# // Copyright 2023 Google LLC
# // SPDX-License-Identifier: Apache-2.0
#
fn say_hello(name: String) {
    println!("Hello {name}")
}

fn main() {
    let name = String::from("Alice");
    say_hello(name);
    // say_hello(name);
}
<details>
  • 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:

c++
std::string s1 = "Cpp";
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:

bob
 Stack                             Heap
.- - - - - - - - - - - - - -.     .- - - - - - - - - - - -.
:                           :     :                       :
:    s1                     :     :                       :
:   +-----------+-------+   :     :   +----+----+----+    :
:   | ptr       |   o---+---+--+--+-->| C  | p  | p  |    :
:   | len       |     3 |   :     :   +----+----+----+    :
:   | capacity  |     3 |   :     :                       :
:   +-----------+-------+   :     :                       :
:                           :     `- - - - - - - - - - - -'
`- - - - - - - - - - - - - -'

After copy-assignment:

bob
 Stack                             Heap
.- - - - - - - - - - - - - -.     .- - - - - - - - - - - -.
:                           :     :                       :
:    s1                     :     :                       :
:   +-----------+-------+   :     :   +----+----+----+    :
:   | ptr       |   o---+---+--+--+-->| C  | p  | p  |    :
:   | len       |     3 |   :     :   +----+----+----+    :
:   | capacity  |     3 |   :     :                       :
:   +-----------+-------+   :     :                       :
:                           :     :                       :
:    s2                     :     :                       :
:   +-----------+-------+   :     :   +----+----+----+    :
:   | ptr       |   o---+---+-----+-->| C  | p  | p  |    :
:   | len       |     3 |   :     :   +----+----+----+    :
:   | capacity  |     3 |   :     :                       :
:   +-----------+-------+   :     :                       :
:                           :     `- - - - - - - - - - - -'
`- - - - - - - - - - - - - -'

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 that is being copied or moved.

</details>