Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 4

Rust Ownership and Move Semantics

Recommended resources

Hello! Welcome back to your journey into Rust.

In our last lesson, you did a great job distinguishing between String and &str. We saw that a String is owned, growable data, while a &str is a borrowed, immutable view of that data. You might have been wondering why Rust makes this distinction. The answer lies in its most famous and powerful feature: the ownership model.

Today, we're going to dive into that model. You will learn what ownership is, how it enables Rust to guarantee memory safety without a garbage collector, and you'll understand the concept of move semantics—how Rust transfers ownership of data between variables. This is a fundamental concept that, once grasped, will make writing Rust feel much more intuitive. It's the key to working with the compiler, not against it.

1. Memory, Ownership, and The Three Rules

Coming from Java and C, you've seen two different approaches to memory management. In C, you manually allocate and free memory (malloc/free), which is fast but can lead to bugs like memory leaks or using freed memory. In Java, a garbage collector runs in the background, cleaning up unused objects, which is safer but can introduce performance overhead.

Rust takes a third approach: memory is managed through a system of ownership with a set of rules that the compiler checks at compile time. This means you get the performance of C-style manual management with the memory safety guarantees of a garbage-collected language, all without a runtime garbage collector.

To understand ownership, we first need a quick refresher on how a program uses memory. The following video provides a great analogy for the two main memory areas: the stack and the heap.

Learning Rust: Memory, Ownership and Borrowing

This video from the YouCodeThings channel introduces the concepts of the stack and the heap using a simple analogy. It then lays out the three fundamental rules of ownership that govern how Rust manages this memory.

Watch from the beginning to 03:33. Focus on: The 'desk and paper' analogy for the stack (00:34). The 'locker room' analogy for the heap (02:11). The three rules of ownership presented at the end (03:13).

As the video explained, the rules of ownership are simple but have profound implications:

  1. Each value in Rust has a variable that’s called its owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value will be dropped (i.e., the memory is freed).

These three rules are the bedrock of Rust's memory safety. Let's see how they work in practice.


2. Move Semantics: Transferring Ownership

Now for the main event: what happens when you assign a value from one variable to another? Based on the second rule—"There can only be one owner at a time"—we can't just have two variables owning the same piece of data.

Consider this code with the String type we learned about, which stores its text on the heap:

let s1 = String::from("hello");
let s2 = s1;

In many languages, this would create a copy of the string. In Rust, something different happens: ownership is moved.

After let s2 = s1;, s2 is now the owner of the string data. The second ownership rule dictates that s1 can no longer be the owner. To enforce this, the compiler considers s1 invalidated and will not let you use it again.

This visual diagram illustrates what's happening. The data on the heap isn't copied; only the pointer, length, and capacity on the stack are. Then, the original variable is invalidated.

Rust Move Semantics Memory Representation
This diagram shows ownership of heap data being transferred from `fruit1` to `fruit2`. The data itself is not copied; `fruit1` simply becomes invalid after the move.

If you try to use s1 after the move, the compiler will stop you with an error. This is one of the most common errors for new Rust programmers, and it directly prevents potential bugs like "double-freeing" memory.

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // Ownership of the String data is moved from s1 to s2

    // This next line will not compile! Try it yourself.
    // println!("s1 is: {}", s1);
    
    println!("s2 is: {}", s2); // This is fine, s2 is the owner.
}

This transfer of ownership is called a move.


3. Move vs. Copy: An Important Distinction

You might be thinking, "Wait, in the first lesson, I could do this with numbers and it worked fine!" You are absolutely right.

let x = 5;
let y = x;

println!("x = {}, y = {}", x, y); // This works perfectly!

Why does this work, but the String example fails?

The reason is that some types, like integers, booleans, and floating-point numbers, have a special trait called Copy. For these types, which have a known, fixed size and live entirely on the stack, Rust makes a full copy of the data when you reassign them. It's cheap to do, so there's no need to invalidate the original variable.

Types that manage heap data, like String, do not implement the Copy trait. For them, Rust defaults to the safer and more performant move semantic.

The following reading explains this distinction clearly. It's written in an informal style that tackles the exact points of confusion you might be having right now.

Rust Mutability, Moving and Borrowing - The Straight Dope

This article, 'Rust Mutability, Moving and Borrowing - The Straight Dope', provides a fantastic, no-nonsense explanation of move semantics and the 'gotcha' with Copy types.

Read the sections titled 'What the hell is a "move?"' and 'BUT WAIT - YOU WILL PULL YOUR HAIR OUT OVER THIS!!!!'. They perfectly capture the experience of learning this concept.


4. Ownership and Functions

The same ownership rules apply when passing values to functions.

  • Passing a Copy type (like i32) to a function copies the value.
  • Passing an owned type (like String) to a function moves ownership to the function's parameter. The original variable becomes invalid, just as if you had assigned it to another variable.

This video demonstrates this concept visually.

Learning Rust: Memory, Ownership and Borrowing

Let's return to the YouCodeThings video to see how ownership is transferred when a value is passed to a function.

Watch the section from 05:31 to 06:24. It walks through what happens on the stack when a function takes ownership of a string and the scope ends.

Here's a code example demonstrating this.

fn main() {
    let my_string = String::from("This is my string");

    // takes_ownership will take ownership of my_string
    takes_ownership(my_string);

    // This line would cause a compile error because my_string's ownership was moved
    // println!("{}", my_string); 
}

// This function takes ownership of the String passed to it.
fn takes_ownership(some_string: String) {
    println!("I now own this string: '{}'", some_string);
} // Here, some_string goes out of scope and `drop` is called. The memory is freed.

Hands-On Activity: Fight the Compiler!

The best way to learn ownership is to see the compiler enforce it. Let's do a quick exercise in your rust_chess project.

  1. Open src/main.rs and clear out the main function.
  2. Create a String variable, for example, let piece = String::from("Pawn");.
  3. Create a new variable and move the ownership, e.g., let moved_piece = piece;.
  4. Now, try to print the original piece variable.
  5. Run cargo run and read the compiler error carefully. It will tell you exactly what happened ("use of moved value").
  6. Comment out the line that caused the error.
  7. Now, define a simple function outside main called print_piece that takes one parameter of type String and prints it.
  8. In main, create another String and call print_piece with it.
  9. After the function call, try to print that String again in main.
  10. Run cargo run again. Observe the similar "use of moved value" error.

This exercise forces you to confront the exact scenarios where ownership moves cause compiler errors, helping you build an intuition for the rules.


Conclusion

Fantastic work! You've just tackled the single most unique and challenging concept in Rust. It might feel restrictive at first, but this system is what makes Rust so powerful and safe.

Key Takeaways:

  • Rust uses an ownership model with three rules to manage memory safely without a garbage collector.
  • Values that manage heap resources (like String) are moved when assigned to a new variable or passed to a function. The original owner becomes invalid.
  • Simple values stored entirely on the stack (like i32, bool) are copied. The original variable remains valid.
  • The Rust compiler enforces these rules at compile time, preventing a whole class of memory-related bugs.

You're probably thinking, "If ownership is always moved, how can I use a value in multiple places without making copies all the time?" That's the perfect question, and it leads directly to our next topic. In the last lesson, you saw &str as a "borrowed" view. In the next lesson, we will formalize this concept by exploring borrowing and references, Rust's solution for accessing data without taking ownership.