class: title # Ownership, Borrowing, and Lifetimes ## Scott Rixner and Alan Cox --- layout: true --- ## The Core of Rust Ownership is Rust's most unique feature. * It enables Rust to make memory safety guarantees without needing a garbage collector. * Three Pillars: 1. **Ownership:** Who cleans up this data? 2. **Borrowing:** How can I access data I don't own? 3. **Lifetimes:** How long is this reference valid? --- ## Memory Management Strategies * Garbage Collection (Java, Python, Go) * Runtime tracks memory usage and cleans up. * **Pro:** Easy for development. **Con:** Runtime overhead, pauses. * Explicit Management (C, C++) * Programmer manually allocates/frees memory. * **Pro:** Fast, control. **Con:** Use-after-free, memory leaks, double-free. * Ownership (Rust) * Compiler enforces rules at compile time. * **Pro:** Fast, safe. **Con:** Steeper learning curve. --- ## Ownership Rules Three fundamental rules govern memory in Rust: 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**. --- ## Memory Allocation In languages with a Garbage Collector (GC), the GC tracks and cleans up unused memory. .twocolumn[ .col[ In C, you must pair `malloc` and `free`. * Forgot to `free`: memory leak. * `free` too early: invalid variable. * `free` twice: double free error. ] .col[ In Rust, memory is freed automatically. * Memory is automatically returned once the variable that owns it goes out of scope. * Rust calls a special function `drop` at the closing curly bracket. ] ] --- ## Examples Use Strings There are two types of strings: ```rust let s1: &str = "hello"; let s2: String = String::from(s1); ``` * String literals (`"hello"`) are immutable and hardcoded into the binary (stack/static). * `String` is growable and heap-allocated. --- ## Move Semantics ```rust let s1 = String::from("hello"); let s2 = s1; ``` * `s1` holds a pointer, length, and capacity on the stack. The data is on the heap. * When we assign `s2 = s1`, we copy the *stack* data (pointer, len, cap). * We do **not** copy the heap data. **Problem:** If both `s1` and `s2` go out of scope, they both try to free the same memory (double free). --- ## Ownership Transfer (Move) To ensure memory safety, Rust considers `s1` *invalid* after `let s2 = s1;`. ```rust let s1 = String::from("hello"); let s2 = s1; println!("{}, world!", s1); // Compile Error! ``` * Rust doesn't need to clean up `s1` when it goes out of scope. * Ownership was **moved** from `s1` to `s2`. * Only `s2` will free the memory. --- ## Clone If we *do* want a deep copy of the heap data, we use `clone`. ```rust let s1 = String::from("hello"); let s2 = s1.clone(); println!("s1 = {}, s2 = {}", s1, s2); ``` * This works fine. * It may be expensive (copying heap data). --- class: middle ## Question: Implementation Trade-offs ```rust let s1 = String::from("hello"); ``` * `s1` holds a pointer, length, and capacity on the stack. The data is on the heap. Why aren't the `String`'s length and capacity stored on the heap? --- ## Copy Types In contrast, `let y = x` works for integers. ```rust let x = 5; let y = x; println!("x = {}, y = {}", x, y); // Valid ``` * Types entirely stored on the stack implement the `Copy` trait. * If a type implements `Copy`, variables are not moved; they are bitwise copied. * Examples: Integers, Booleans, Floating point, Chars, Tuples (if they only contain `Copy` types). --- ## Ownership and Functions Passing a value to a function is just like assignment. It will **move** or **copy**. ```rust fn main() { let s = String::from("hello"); // s comes into scope takes_ownership(s); // s's value moves into the function... // ... and is no longer valid here let x = 5; // x comes into scope makes_copy(x); // x would move into the function, // but i32 is Copy, so it's okay to use x afterward } ``` --- ## Return Values and Scope Returning values can also transfer ownership. ```rust fn main() { let s1 = gives_ownership(); // gives_ownership moves its return // value into s1 let s2 = String::from("hello"); // s2 comes into scope let s3 = takes_and_gives_back(s2); // s2 is moved into // takes_and_gives_back, which also // moves its return value into s3 } ``` **The Issue:** Taking ownership and then returning ownership with every function is tedious to keep track of (for the programmer). --- ## Explicit Drop Semantics We know `drop` is called when a variable goes out of scope, but ownership changes complicate this. * Variable goes out of scope: `drop` is called. * Ownership Moved: The original variable no longer owns the data. It will **not** call `drop`. The new owner is now responsible. * Moved into Function: The function parameter becomes the owner. When the function ends, it calls `drop`. * Moved out of Function: Ownership is returned to the caller. The function does **not** call `drop`. --- ## Drop Example ```rust fn main() { let s1 = String::from("hello"); // s1 owns "hello" let s2 = s1; // Ownership moves to s2. s1 is now invalid. // If s1 went out of scope here, nothing happens (it's empty). do_something(s2); // s2 moves into do_something. // s2 is now invalid in main. // do_something cleans up the memory when it finishes. } // End of main. Nothing to drop! (s1 and s2 gave away ownership) fn do_something(s: String) { println!("{}", s); } // s goes out of scope here. drop is called. Memory freed. ``` --- ## Borrowing ```rust fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); // Pass a reference println!("The length of '{}' is {}.", s1, len); // s1 is still valid! } fn calculate_length(s: &String) -> usize { // s is a reference to a String s.len() } ``` * **Borrowing** allows the use of a value without taking ownership of it. * Use references (`&`). --- ## References ```rust let s1 = String::from("hello"); let len = calculate_length(&s1); // &s1 is a reference to s1 ``` * `&s1` creates a reference that refers to the value of `s1` but does not own it. * Because it does not own it, the value it points to will not be dropped when the reference stops being used. * The compiler guarantees that references will never outlive the data they point to. * References are immutable by default. --- ## Reference Rules in Action ```rust fn main() { let s1 = String::from("hello"); // Valid: multiple immutable references let r1 = &s1; let r2 = &s1; println!("{} and {}", r1, r2); // Error: cannot modify via immutable reference // r1.push_str(", world"); let r3; { let s2 = String::from("temporary"); r3 = &s2; } // s2 is dropped here // Error: r3 refers to dropped memory // println!("{}", r3); } ``` --- ## The Rules of Borrowing At any given time, you can have **either but not both** of: * One mutable reference. * Any number of immutable references. References must always be valid (no dangling references). * The compiler guarantees this for you! --- ## Mutable References ```rust fn change(some_string: &mut String) { some_string.push_str(", world"); } fn main() { let mut s = String::from("hello"); change(&mut s); } ``` * Variable must be `mut`. * Reference must be `&mut`. --- ## Combining Reference Types ```rust let mut s = String::from("hello"); let r1 = &s; let r2 = &s; let r3 = &mut s; // Error! println!("{}, {}, and {}", r1, r2, r3); ``` * You also cannot have a mutable reference while you have an immutable one. * Users of immutable references (`r1`, `r2`) expect the value to remain unchanged. --- class: middle ## Question: Combined References ```rust let mut s = String::from("hello"); let r1 = &s; let r2 = &s; println!("{} and {}", r1, r2); let r3 = &mut s; // Error? println!("{}", r3); ``` Will this variant compile? --- ## Dangling References ```rust fn dangle() -> &String { let s = String::from("hello"); &s // Error: s goes out of scope and is dropped. Reference would point to nothing. } ``` * In languages with pointers, it's easy to keep a pointer that references memory that has been freed. * The Rust compiler guarantees no dangling references. --- class: middle ## Questions: Implications of the Ownership Rules What are the implications of the ownership rules on the implementation of common data structures? Will this make Rust significantly harder to use? --- ## Lifetimes * Every reference in Rust has a **lifetime**, which is the scope for which that reference is valid. * Most of the time, lifetimes are implicit and inferred (lifetime elision). * We must annotate lifetimes when the lifetimes of references could be related in a few different ways. --- ## Lifetime Annotation Syntax ```rust &i32 // a reference with an implicit lifetime &'a i32 // a reference with an explicit lifetime &'a mut i32 // a mutable reference with an explicit lifetime ``` * Start with an apostrophe `'`. * Examples: `'a`, `'b`, `'life`. * Placed after `&`. --- ## The Borrow Checker ```rust { let r; // ---------+-- 'a { // | let x = 5; // -+-- 'b | r = &x; // | | } // -+ | println!("r: {}", r); // | } // ---------+ ``` * The compiler uses a borrow checker to determine if all borrows are valid. * Here, `r` has lifetime `'a` and refers to `x` with lifetime `'b`. * Block `'b` is smaller than `'a`. * The subject of the reference (`x`) doesn't live long enough. --- ## Generic Lifetimes in Functions ```rust fn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { x } else { y } } ``` * Attempt to return the longer of two string slices. * **Error:** Rust doesn't know if the return reference refers to `x` or `y`. * It therefore doesn't know the lifetime of the return value. --- ## Fixing the Function We tell Rust that `x`, `y`, and the return value all live at least as long as lifetime `'a`. ```rust fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } ``` * The returned reference will be valid as long as *both* parameters are valid. * In practice, it creates a relationship between the input lifetimes and the output lifetime. --- ## Lifetimes in Structs ```rust struct ImportantExtract<'a> { part: &'a str, } fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().expect("Could not find a '.'"); let i = ImportantExtract { part: first_sentence, }; } ``` * Structs can hold references, but we must add a lifetime annotation. * An instance of `ImportantExtract` cannot outlive the reference it holds in `part`. --- ## Lifetime Elision * Predictable patterns are encoded into the compiler. * Three rules of elision: 1. Each parameter that is a reference gets its own lifetime parameter. 2. If there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters. 3. If there are multiple input lifetime parameters, but one of them is `&self` or `&mut self`, the lifetime of `self` is assigned to all output lifetime parameters. --- ## Static Lifetime ```rust let s: &'static str = "I have a static lifetime."; ``` * One special lifetime: `'static`. * The reference can live for the entire duration of the program. * String literals have the `'static` lifetime. * If compiler suggests adding `'static` to fix a lifetime issue, think twice. Usually, it means you have a dangling reference you are trying to hack, or you should move ownership instead. --- ## Summary * **Ownership** ensures memory safety by cleaning up values when owners go out of scope. * **Borrowing** (References) allows access without taking ownership. * **Borrow Checker** enforces rules: * Many immutable refs OR one mutable ref. * References must always be valid. * **Lifetimes** are the scope validation mechanism for references. --- ## Practice and Understanding This model is the hardest part of Rust. * If you fight the borrow checker, you are usually fighting a memory bug that C++ would have let you write (and crash later). * **Practice:** Try writing a linked list or a tree. You will quickly encounter the edges of ownership.