Chapter 4: Ownership - THE MOST IMPORTANT CONCEPT

Understanding Rust’s Unique Memory Management

Learning Objectives

By the end of this chapter, you’ll be able to:

  • Understand ownership rules and how they differ from C++/.NET memory management
  • Work confidently with borrowing and references
  • Navigate lifetime annotations and understand when they’re needed
  • Transfer ownership safely between functions and data structures
  • Debug common ownership errors with confidence
  • Apply ownership principles to write memory-safe, performant code

Why Ownership Matters: The Problem It Solves

Memory Management Comparison

LanguageMemory ManagementCommon IssuesPerformanceSafety
C++Manual (new/delete, RAII)Memory leaks, double-free, dangling pointersHighRuntime crashes
C#/.NETGarbage CollectorGC pauses, memory pressureMediumRuntime exceptions
RustCompile-time ownershipCompiler errors (not runtime!)HighCompile-time safety

The Core Problem

// C++ - Dangerous code that compiles
std::string* dangerous() {
    std::string local = "Hello";
    return &local;  // ❌ Returning reference to local variable!
}
// This compiles but crashes at runtime

// C# - Memory managed but can still have issues
class Manager {
    private List<string> items;
    
    public IEnumerable<string> GetItems() {
        items = null;  // Oops!
        return items;  // ❌ NullReferenceException at runtime
    }
}
#![allow(unused)]
fn main() {
// Rust - Won't compile, saving you from runtime crashes
fn safe_rust() -> &str {
    let local = String::from("Hello");
    &local  // ❌ Compile error: `local` does not live long enough
}
// Error caught at compile time!
}

The Three Rules of Ownership

Rule 1: Each Value Has a Single Owner

#![allow(unused)]
fn main() {
let s1 = String::from("Hello");    // s1 owns the string
let s2 = s1;                       // Ownership moves to s2
// println!("{}", s1);             // ❌ Error: value borrowed after move

// Compare to C++:
// std::string s1 = "Hello";       // s1 owns the string  
// std::string s2 = s1;            // s2 gets a COPY (expensive!)
// std::cout << s1;                // ✅ Still works, s1 unchanged
}

Rule 2: There Can Only Be One Owner at a Time

fn take_ownership(s: String) {     // s comes into scope
    println!("{}", s);
}   // s goes out of scope and `drop` is called, memory freed

fn main() {
    let s = String::from("Hello");
    take_ownership(s);             // s's value moves into function
    // println!("{}", s);          // ❌ Error: value borrowed after move
}

Rule 3: When the Owner Goes Out of Scope, the Value is Dropped

#![allow(unused)]
fn main() {
{
    let s = String::from("Hello");  // s comes into scope
    // do stuff with s
}                                   // s goes out of scope, memory freed automatically
}

Move Semantics: Ownership Transfer

Understanding Moves

#![allow(unused)]
fn main() {
// Primitive types implement Copy trait
let x = 5;
let y = x;              // x is copied, both x and y are valid
println!("x: {}, y: {}", x, y);  // ✅ Works fine

// Complex types move by default
let s1 = String::from("Hello");
let s2 = s1;            // s1 is moved to s2
// println!("{}", s1);  // ❌ Error: value borrowed after move
println!("{}", s2);     // ✅ Only s2 is valid

// Clone when you need a copy
let s3 = String::from("World");
let s4 = s3.clone();    // Explicit copy
println!("s3: {}, s4: {}", s3, s4);  // ✅ Both valid
}

Copy vs Move Types

#![allow(unused)]
fn main() {
// Types that implement Copy (stored on stack)
let a = 5;        // i32
let b = true;     // bool
let c = 'a';      // char
let d = (1, 2);   // Tuple of Copy types

// Types that don't implement Copy (may use heap)
let e = String::from("Hello");     // String
let f = vec![1, 2, 3];            // Vec<i32>
let g = Box::new(42);             // Box<i32>

// Copy types can be used after assignment
let x = a;  // a is copied
println!("a: {}, x: {}", a, x);   // ✅ Both work

// Move types transfer ownership
let y = e;  // e is moved
// println!("{}", e);             // ❌ Error: moved
}

References and Borrowing

Immutable References (Shared Borrowing)

fn calculate_length(s: &String) -> usize {  // s is a reference
    s.len()
}   // s goes out of scope, but doesn't own data, so nothing happens

fn main() {
    let s1 = String::from("Hello");
    let len = calculate_length(&s1);        // Pass reference
    println!("Length of '{}' is {}.", s1, len);  // ✅ s1 still usable
}

Mutable References (Exclusive Borrowing)

fn change(s: &mut String) {
    s.push_str(", world");
}

fn main() {
    let mut s = String::from("Hello");
    change(&mut s);                         // Pass mutable reference
    println!("{}", s);                      // Prints: Hello, world
}

The Borrowing Rules

Rule 1: Either one mutable reference OR any number of immutable references

#![allow(unused)]
fn main() {
let mut s = String::from("Hello");

// ✅ Multiple immutable references
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);  // OK

// ❌ Cannot have mutable reference with immutable ones
let r3 = &s;
let r4 = &mut s;  // Error: cannot borrow as mutable
}

Rule 2: References must always be valid (no dangling references)

#![allow(unused)]
fn main() {
fn dangle() -> &String {        // Returns reference to String
    let s = String::from("hello");
    &s                          // ❌ Error: `s` does not live long enough
}   // s is dropped, reference would be invalid

// ✅ Solution: Return owned value
fn no_dangle() -> String {
    let s = String::from("hello");
    s                           // Move s out, no reference needed
}
}

Reference Patterns in Practice

// Good: Take references when you don't need ownership
fn print_length(s: &str) {      // &str works with String and &str
    println!("Length: {}", s.len());
}

// Good: Take mutable reference when you need to modify
fn append_exclamation(s: &mut String) {
    s.push('!');
}

// Sometimes you need ownership
fn take_and_process(s: String) -> String {
    // Do expensive processing that consumes s
    format!("Processed: {}", s.to_uppercase())
}

fn main() {
    let mut text = String::from("Hello");
    
    print_length(&text);        // Borrow immutably
    append_exclamation(&mut text);  // Borrow mutably  
    
    let result = take_and_process(text);  // Transfer ownership
    // text is no longer valid here
    println!("{}", result);
}

Lifetimes: Ensuring Reference Validity

Why Lifetimes Exist

#![allow(unused)]
fn main() {
// The compiler needs to ensure this is safe:
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
// Question: How long should the returned reference live?
}

Lifetime Annotation Syntax

#![allow(unused)]
fn main() {
// Explicit lifetime annotations
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

// The lifetime 'a means:
// - x and y must both live at least as long as 'a
// - The returned reference will live as long as 'a
// - 'a is the shorter of the two input lifetimes
}

Lifetime Elision Rules (When You Don’t Need Annotations)

Rule 1: Each reference parameter gets its own lifetime

#![allow(unused)]
fn main() {
// This:
fn first_word(s: &str) -> &str { /* ... */ }
// Is actually this:
fn first_word<'a>(s: &'a str) -> &'a str { /* ... */ }
}

Rule 2: If there’s exactly one input lifetime, it’s assigned to all outputs

#![allow(unused)]
fn main() {
// These are equivalent:
fn get_first(list: &Vec<String>) -> &String { &list[0] }
fn get_first<'a>(list: &'a Vec<String>) -> &'a String { &list[0] }
}

Rule 3: Methods with &self give output the same lifetime as self

#![allow(unused)]
fn main() {
impl<'a> Person<'a> {
    fn get_name(&self) -> &str {  // Implicitly &'a str
        self.name
    }
}
}

Complex Lifetime Examples

#![allow(unused)]
fn main() {
// Multiple lifetimes
fn compare_and_return<'a, 'b>(
    x: &'a str, 
    y: &'b str, 
    return_first: bool
) -> &'a str {  // Always returns something with lifetime 'a
    if return_first { x } else { y }  // ❌ Error: y has wrong lifetime
}

// Fixed version - both inputs must have same lifetime
fn compare_and_return<'a>(
    x: &'a str, 
    y: &'a str, 
    return_first: bool
) -> &'a str {
    if return_first { x } else { y }  // ✅ OK
}
}

Structs with Lifetimes

// Struct holding references needs lifetime annotation
struct ImportantExcerpt<'a> {
    part: &'a str,  // This reference must live at least as long as the struct
}

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
    
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part  // Returns reference with same lifetime as &self
    }
}

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 = ImportantExcerpt {
        part: first_sentence,
    };
    // i is valid as long as novel is valid
}

Static Lifetime

#![allow(unused)]
fn main() {
// 'static means the reference lives for the entire program duration
let s: &'static str = "I have a static lifetime.";  // String literals

// Static variables
static GLOBAL_COUNT: i32 = 0;
let count_ref: &'static i32 = &GLOBAL_COUNT;

// Sometimes you need to store static references
struct Config {
    name: &'static str,    // Must be a string literal or static
}
}

Advanced Ownership Patterns

Returning References from Functions

#![allow(unused)]
fn main() {
// ❌ Cannot return reference to local variable
fn create_and_return() -> &str {
    let s = String::from("hello");
    &s  // Error: does not live long enough
}

// ✅ Return owned value instead
fn create_and_return_owned() -> String {
    String::from("hello")
}

// ✅ Return reference to input (with lifetime)
fn get_first_word(text: &str) -> &str {
    text.split_whitespace().next().unwrap_or("")
}
}

Ownership with Collections

fn main() {
    let mut vec = Vec::new();
    
    // Adding owned values
    vec.push(String::from("hello"));
    vec.push(String::from("world"));
    
    // ❌ Cannot move out of vector by index
    // let first = vec[0];  // Error: cannot move
    
    // ✅ Borrowing is fine
    let first_ref = &vec[0];
    println!("First: {}", first_ref);
    
    // ✅ Clone if you need ownership
    let first_owned = vec[0].clone();
    
    // ✅ Or use into_iter() to transfer ownership
    for item in vec {  // vec is moved here
        println!("Owned item: {}", item);
    }
    // vec is no longer usable
}

Splitting Borrows

#![allow(unused)]
fn main() {
// Sometimes you need to borrow different parts of a struct
struct Point {
    x: f64,
    y: f64,
}

impl Point {
    // ❌ This won't work - can't return multiple mutable references
    // fn get_coords_mut(&mut self) -> (&mut f64, &mut f64) {
    //     (&mut self.x, &mut self.y)
    // }
    
    // ✅ This works - different fields can be borrowed separately
    fn update_coords(&mut self, new_x: f64, new_y: f64) {
        self.x = new_x;  // Borrow x mutably
        self.y = new_y;  // Borrow y mutably (different field)
    }
}
}

Common Ownership Patterns and Solutions

Pattern 1: Function Parameters

#![allow(unused)]
fn main() {
// ❌ Don't take ownership unless you need it
fn process_text(text: String) -> usize {
    text.len()  // We don't need to own text for this
}

// ✅ Better: take a reference
fn process_text(text: &str) -> usize {
    text.len()
}

// ✅ When you do need ownership:
fn store_text(text: String) -> Box<String> {
    Box::new(text)  // We're storing it, so ownership makes sense
}
}

Pattern 2: Return Values

#![allow(unused)]
fn main() {
// ✅ Return owned values when creating new data
fn create_greeting(name: &str) -> String {
    format!("Hello, {}!", name)
}

// ✅ Return references when extracting from input
fn get_file_extension(filename: &str) -> Option<&str> {
    filename.split('.').last()
}
}

Pattern 3: Structs Holding Data

// ✅ Own data when struct should control lifetime
#[derive(Debug)]
struct User {
    name: String,      // Owned
    email: String,     // Owned
}

// ✅ Borrow when data lives elsewhere  
#[derive(Debug)]
struct UserRef<'a> {
    name: &'a str,     // Borrowed
    email: &'a str,    // Borrowed
}

// Usage
fn main() {
    // Owned version - can outlive source data
    let user = User {
        name: String::from("Alice"),
        email: String::from("alice@example.com"),
    };
    
    // Borrowed version - tied to source data lifetime
    let name = "Bob";
    let email = "bob@example.com";
    let user_ref = UserRef { name, email };
}

Debugging Ownership Errors

Common Error Messages and Solutions

1. “Value borrowed after move”

#![allow(unused)]
fn main() {
// ❌ Problem
let s = String::from("hello");
let s2 = s;           // s moved here
println!("{}", s);    // Error: value borrowed after move

// ✅ Solutions
// Option 1: Use references
let s = String::from("hello");
let s2 = &s;          // Borrow instead
println!("{} {}", s, s2);

// Option 2: Clone when you need copies
let s = String::from("hello");
let s2 = s.clone();   // Explicit copy
println!("{} {}", s, s2);
}

2. “Cannot borrow as mutable”

#![allow(unused)]
fn main() {
// ❌ Problem
let s = String::from("hello");  // Immutable
s.push_str(" world");          // Error: cannot borrow as mutable

// ✅ Solution: Make it mutable
let mut s = String::from("hello");
s.push_str(" world");
}

3. “Borrowed value does not live long enough”

#![allow(unused)]
fn main() {
// ❌ Problem
fn get_string() -> &str {
    let s = String::from("hello");
    &s  // Error: does not live long enough
}

// ✅ Solutions
// Option 1: Return owned value
fn get_string() -> String {
    String::from("hello")
}

// Option 2: Use string literal (static lifetime)
fn get_string() -> &'static str {
    "hello"
}
}

Tools for Understanding Ownership

#![allow(unused)]
fn main() {
fn debug_ownership() {
    let s1 = String::from("hello");
    println!("s1 created");
    
    let s2 = s1;  // Move occurs here
    println!("s1 moved to s2");
    // println!("{}", s1);  // This would error
    
    let s3 = &s2;  // Borrow s2
    println!("s2 borrowed as s3: {}", s3);
    
    drop(s2);  // Explicit drop
    println!("s2 dropped");
    // println!("{}", s3);  // This would error - s2 was dropped
}
}

Performance Implications

Zero-Cost Abstractions

#![allow(unused)]
fn main() {
// All of these have the same runtime performance:

// Direct access
let vec = vec![1, 2, 3, 4, 5];
let sum1 = vec[0] + vec[1] + vec[2] + vec[3] + vec[4];

// Iterator (zero-cost abstraction)
let sum2: i32 = vec.iter().sum();

// Reference passing (no copying)
fn sum_vec(v: &Vec<i32>) -> i32 {
    v.iter().sum()
}
let sum3 = sum_vec(&vec);

// All compile to similar assembly code!
}

Memory Layout Guarantees

#![allow(unused)]
fn main() {
// Rust guarantees memory layout
#[repr(C)]  // Compatible with C struct layout
struct Point {
    x: f64,     // Guaranteed to be first
    y: f64,     // Guaranteed to be second
}

// No hidden vtables, no GC headers
// What you see is what you get in memory
}

Key Takeaways

  1. Ownership prevents entire classes of bugs at compile time
  2. Move semantics are default - be explicit when you want copies
  3. Borrowing allows safe sharing without ownership transfer
  4. Lifetimes ensure references are always valid but often inferred
  5. The compiler is your friend - ownership errors are caught early
  6. Zero runtime cost - all ownership checks happen at compile time

Mental Model Summary

#![allow(unused)]
fn main() {
// Think of ownership like keys to a house:
let house_keys = String::from("keys");        // You own the keys

let friend = house_keys;                      // You give keys to friend
// house_keys is no longer valid             // You no longer have keys

let borrowed_keys = &friend;                  // Friend lets you borrow keys
// friend still owns keys                     // Friend still owns them

drop(friend);                                 // Friend moves away
// borrowed_keys no longer valid             // Your borrowed keys invalid
}

Exercises

Exercise 1: Ownership Transfer Chain

Create a program that demonstrates ownership transfer through a chain of functions:

// Implement these functions following ownership rules
fn create_message() -> String {
    // Create and return a String
}

fn add_greeting(message: String) -> String {
    // Take ownership, add "Hello, " prefix, return new String
}

fn add_punctuation(message: String) -> String {
    // Take ownership, add "!" suffix, return new String
}

fn print_and_consume(message: String) {
    // Take ownership, print message, let it be dropped
}

fn main() {
    // Chain the functions together
    // create -> add_greeting -> add_punctuation -> print_and_consume
    
    // Try to use the message after each step - what happens?
}

Exercise 2: Reference vs Ownership

Fix the ownership issues in this code:

fn analyze_text(text: String) -> (usize, String) {
    let word_count = text.split_whitespace().count();
    let uppercase = text.to_uppercase();
    (word_count, uppercase)
}

fn main() {
    let article = String::from("Rust is a systems programming language");
    
    let (count, upper) = analyze_text(article);
    
    println!("Original: {}", article);  // ❌ This should work but doesn't
    println!("Word count: {}", count);
    println!("Uppercase: {}", upper);
    
    // Also make this work:
    let count2 = analyze_text(article).0;  // ❌ This should also work
}

Exercise 3: Lifetime Annotations

Implement a function that finds the longest common prefix of two strings:

// Fix the lifetime annotations
fn longest_common_prefix(s1: &str, s2: &str) -> &str {
    let mut i = 0;
    let s1_chars: Vec<char> = s1.chars().collect();
    let s2_chars: Vec<char> = s2.chars().collect();
    
    while i < s1_chars.len() && 
          i < s2_chars.len() && 
          s1_chars[i] == s2_chars[i] {
        i += 1;
    }
    
    &s1[..i]  // Return slice of first string
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_common_prefix() {
        assert_eq!(longest_common_prefix("hello", "help"), "hel");
        assert_eq!(longest_common_prefix("rust", "ruby"), "ru");
        assert_eq!(longest_common_prefix("abc", "xyz"), "");
    }
}

fn main() {
    let word1 = String::from("programming");
    let word2 = "program";
    
    let prefix = longest_common_prefix(&word1, word2);
    println!("Common prefix: '{}'", prefix);
    
    // Both word1 and word2 should still be usable here
    println!("Word1: {}, Word2: {}", word1, word2);
}

Next Up: In Chapter 5, we’ll explore smart pointers - Rust’s tools for more complex memory management scenarios when simple ownership isn’t enough.