Ownership and Borrowing in Rust

Introduction

  • Rust's most distinctive feature: ownership system
  • Enables memory safety without garbage collection
  • Compile-time guarantees with zero runtime cost
  • Three key concepts: ownership, borrowing, and lifetimes

Prework

Prework Readings

Read the following sections from "The Rust Programming Language" book:

Pre-lecture Reflections

Before class, consider these questions:

  1. What problems does Rust's ownership system solve compared to manual memory management?
  2. How does ownership differ from garbage collection in other languages?
  3. What is the difference between moving and borrowing a value?
  4. When would you use Box<T> instead of storing data on the stack?
  5. How do mutable and immutable references help prevent data races?

Memory Layout: Stack vs Heap

Stack:

  • Fast, fixed-size allocation
  • LIFO (Last In, First Out) structure
  • Stores data with known, fixed size at compile time
  • Examples: integers, booleans, fixed-size arrays

Heap:

  • Slower, dynamic allocation
  • For data with unknown or variable size
  • Allocator finds space and returns a pointer
  • Examples: String, Vec, Box

Stack Memory Example

fn main() {
    let x = 5;           // stored on stack
    let y = true;        // stored on stack
    let z = x;           // copy of value on stack
    println!("{}, {}", x, z);  // both still valid
}
  • Simple types implement Copy trait
  • Assignment creates a copy, both variables remain valid

String and the Heap

Heap Memory: The String Type

Let's look more closely at the String type.

#![allow(unused)]
fn main() {
let s1 = String::from("hello");
}
  • String stores pointer, length, capacity on stack
  • Actual string data stored on heap

In fact we can inspect the memory layout of a String:

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

println!("&s:{:p}", &s);
println!("ptr: {:p}", s.as_ptr());
println!("len: {}", s.len());
println!("capacity: {}\n", s.capacity());

// Let's add some more text to the string
s.push_str(", world!");
println!("&s:{:p}", &s);
println!("ptr: {:p}", s.as_ptr());
println!("len: {}", s.len());
println!("capacity: {}", s.capacity());
}

Shallow Copy with Move

fn main() {
    let s1 = String::from("hello");
    // s1 has three parts on stack:
    // - pointer to heap data
    // - length: 5
    // - capacity: 5
    
    let s2 = s1;  // shallow copy of stack data

    println!("{}", s1);  // ERROR! s1 is no longer valid
    println!("{}", s2);     // OK
}
  • String stores pointer, length, capacity on stack
  • Actual string data stored on heap

Shallow Copy:

  • Copying the pointer, length, and capacity
  • The actual string data is not copied
  • The owner of the string data is transferred to the new structure

#![allow(unused)]
fn main() {
let s1 = String::from("hello");

println!("&s1:{:p}", &s1);
println!("ptr: {:p}", s1.as_ptr());
println!("len: {}", s1.len());
println!("capacity: {}\n", s1.capacity());

let s2 = s1;

println!("&s2:{:p}", &s2);
println!("ptr: {:p}", s2.as_ptr());
println!("len: {}", s2.len());
println!("capacity: {}", s2.capacity());
}

The Ownership Rules

  1. Each value in Rust has an owner
  2. There can only be one owner at a time
  3. When the owner goes out of scope, the value is dropped

These rules prevent:

  • Double free errors
  • Use after free
  • Data races

Ownership Transfer: Move Semantics

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;  // ownership moves from s1 to s2
    
    // s1 is now invalid - compile error if used
    println!("{}", s2);  // OK
    
    // When s2 goes out of scope, memory is freed
}
  • Move prevents double-free
  • Only one owner can free the memory

Clone: Deep Copy

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();  // deep copy of heap data
    
    println!("s1 = {}, s2 = {}", s1, s2);  // both valid
}
  • clone() creates a full copy of heap data
  • Both variables are independent owners
  • More expensive operation

Vec and the Heap

Vec: Dynamic Arrays on the Heap

What is Vec?

  • Vec<T> is Rust's growable, heap-allocated array type
  • Generic over type T (e.g., Vec<i32>, Vec<String>)
  • Contiguous memory allocation for cache efficiency
  • Automatically manages capacity and growth

Three ways to create a Vec:

#![allow(unused)]
fn main() {
// 1. Empty vector with type annotation
let v1: Vec<i32> = Vec::new();

// 2. Using vec! macro with initial values
let v2 = vec![1, 2, 3, 4, 5];

// 3. With pre-allocated capacity
let v3: Vec<i32> = Vec::with_capacity(10);
}

Vec Memory Structure

#![allow(unused)]
fn main() {
let mut v = Vec::new();
v.push(1);
v.push(2);
v.push(3);
    
// Vec structure (on stack):
// - pointer to heap buffer
// - length: 3 (number of elements)
// - capacity: (at least 3, often more)

println!("&v:{:p}", &v);
println!("ptr: {:p}", v.as_ptr());
println!("Length: {}", v.len());
println!("Capacity: {}", v.capacity());

}
  • Pointer: points to heap-allocated buffer
  • Length: number of initialized elements
  • Capacity: total space available before reallocation

Vec Growth and Reallocation

fn main() {
    let mut v = Vec::new();
    println!("Initial capacity: {}", v.capacity());  // 0
    
    v.push(1);
    println!("After 1 push: {}", v.capacity());      // typically 4
    
    v.push(2);
    v.push(3);
    v.push(4);
    v.push(5);  // triggers reallocation
    println!("After 5 pushes: {}", v.capacity());    // typically 8
}
  • Capacity doubles when full (amortized O(1) push)
  • Reallocation: new buffer allocated, old data copied
  • Pre-allocate with with_capacity() to avoid reallocations

Accessing Vec Elements

fn main() {
    let v = vec![10, 20, 30, 40, 50];
    
    // Indexing - panics if out of bounds
    let third = v[2];
    println!("Third element: {}", third);
    
    // Using get() - returns Option<T>
    // Safely handles out of bounds indices
    match v.get(2) {
        Some(value) => println!("Third element: {}", value),
        None => println!("No element at index 2"),
    }
}

Option<T>

Option<T> is an enum that can be either Some(T) or None.

Defined in the standard library as:

#![allow(unused)]
fn main() {
enum Option<T> {
    Some(T),
    None,
}
}

Let's you handle the case where there is no return value.

fn main() {
    let v = vec![1, 2, 3, 4, 5];
    match v.get(0) {
        Some(value) => println!("Element: {}", value),
        None => println!("No element at index"),
    }
}

Modifying Vec Elements

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];
    
    // Direct indexing for modification
    v[0] = 10;
    
    // Adding elements
    v.push(6);           // add to end
    
    // Removing elements
    let last = v.pop();  // remove from end, returns Option<T>
    
    // Insert/remove at position
    v.insert(2, 99);     // insert 99 at index 2
    v.remove(1);         // remove element at index 1
    
    println!("{:?}", v);
}

Vec Ownership

fn main() {
    let v1 = vec![1, 2, 3, 4, 5];
    let v2 = v1;  // ownership moves
    
    // println!("{:?}", v1);  // ERROR!
    println!("{:?}", v2);     // OK
    
    let v3 = v2.clone();      // deep copy
    println!("{:?}, {:?}", v2, v3);  // both OK
}
  • Vec follows same ownership rules as String
  • Move transfers ownership of heap allocation

Functions and Ownership

Functions and Ownership

fn takes_ownership(s: String) {
    println!("{}", s);
}  // s is dropped here

fn main() {
    let s = String::from("hello");
    takes_ownership(s);
    // println!("{}", s);  // ERROR! s was moved
}
  • Passing to function transfers ownership
  • Original variable becomes invalid

Returning Ownership

fn gives_ownership(s: String) -> String {
    let new_s = s + " world";
    new_s  // ownership moves to caller
}

fn main() {
    let s1 = String::from("hello");
    let s2 = gives_ownership(s1);
    println!("{}", s2);  // OK
}
  • Return value transfers ownership out of function
  • Caller becomes new owner

References: Borrowing Without Ownership

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);  // borrow with &
    
    println!("'{}' has length {}", s1, len);  // s1 still valid!
}

fn calculate_length(s: &String) -> usize {
    s.len()
}  // s goes out of scope, but doesn't own data
  • & creates a reference (borrow)
  • Original owner retains ownership
  • Reference allows reading data

Immutable References

fn main() {
    let s = String::from("hello");
    
    let r1 = &s;  // immutable reference
    let r2 = &s;  // another immutable reference
    let r3 = &s;  // yet another
    
    println!("{}, {}, {}", r1, r2, r3);  // all valid

    // Let's take a look at the memory layout
    println!("&s: {:p}, s.as_ptr(): {:p}", &s, s.as_ptr());
    println!("&r1: {:p}, r1.as_ptr(): {:p}", &r1, r1.as_ptr());
    println!("&r2: {:p}, r2.as_ptr(): {:p}", &r2, r2.as_ptr());
    println!("&r3: {:p}, r3.as_ptr(): {:p}", &r3, r3.as_ptr());
}
  • Multiple immutable references allowed simultaneously
  • Cannot modify through immutable reference
// ERROR
fn main() {
    let s = String::from("hello");
    change(&s);
    println!("{}", s);
}

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

Mutable References

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

fn change(s: &mut String) {
    s.push_str(", world");
}
  • &mut creates mutable reference
  • Allows modification of borrowed data

Mutable Reference Restrictions

fn main() {
    let mut s = String::from("hello");
    
    let r1 = &mut s;
    let r2 = &mut s;  // ERROR! Only one mutable reference
    
    println!("{}", r1);
}
  • Only ONE mutable reference at a time
  • Prevents data races at compile time
  • No simultaneous readers when there's a writer

Mixing References: Not Allowed

fn main() {
    let mut s = String::from("hello");
    
    let r1 = &s;      // immutable
    let r2 = &s;      // immutable
    let r3 = &mut s;  // ERROR! Can't have mutable with immutable
    
    println!("{}, {}", r1, r2);
}
  • Cannot have mutable reference while immutable references exist
  • Immutable references expect data won't change

Reference Scopes and Non-Lexical Lifetimes

fn main() {
    let mut s = String::from("hello");
    
    let r1 = &s;
    let r2 = &s;
    println!("{}, {}", r1, r2);
    // r1 and r2 no longer used after this point
    
    let r3 = &mut s;  // OK! Previous references out of scope
    println!("{}", r3);
}
  • Reference scope: from introduction to last use, rather than lexical scope (till end of block)
  • Non-lexical lifetimes allow more flexible borrowing

Vec with References

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];
    
    let first = &v[0];  // immutable borrow
    
    // v.push(6);  // ERROR! Can't mutate while borrowed
    
    println!("First element: {}", first);
    
    v.push(6);  // OK now, first is out of scope
}
  • Borrowing elements prevents mutation of Vec
  • Protects against invalidation (reallocation)

Function Calls: Move vs Reference vs Mutable Reference

fn process_string(s: String) { }         // takes ownership (move)
fn read_string(s: &String) { }           // immutable borrow
fn modify_string(s: &mut String) { }     // mutable borrow

fn main() {
    let mut s = String::from("hello");
    
    read_string(&s);        // borrow
    modify_string(&mut s);  // mutable borrow
    read_string(&s);        // borrow again
    process_string(s);      // move
    // s is now invalid
}

Method Calls with Different Receivers

#![allow(unused)]
fn main() {
impl String {
    // Takes ownership: self
    fn into_bytes(self) -> Vec<u8> { /* ... */ }
    
    // Immutable borrow: &self
    fn len(&self) -> usize { /* ... */ }
    
    // Mutable borrow: &mut self
    fn push_str(&mut self, s: &str) { /* ... */ }
}
}
  • self: method takes ownership (consuming)
  • &self: method borrows immutably
  • &mut self: method borrows mutably

Method Call Examples

  • It can be difficult to understand which ownership rules are being applied to a method call.
fn main() {
    let mut s = String::from("hello");
    
    let len = s.len();           // &self - immutable borrow
    println!("{}, length: {}", s, len);

    s.push_str(" world 🌎");        // &mut self - mutable borrow
    let len = s.len();          // &self - immutable borrow
    println!("{}, length: {}", s, len);
    
    let bytes = s.into_bytes();  // self - takes ownership
    // s is now invalid
    println!("{:?}", bytes);

    let t = String::from_utf8(bytes).unwrap();
    println!("{}", t);
}

Vec Method Patterns

fn main() {
    let mut v = vec![1, 2, 3];
    
    v.push(4);              // &mut self
    let last = v.pop();     // &mut self, returns Option<T>
    let len = v.len();      // &self
    
    // Immutable iteration
    // What happens if you take away the &?
    for item in &v {        // iterate with &Vec
        println!("{}", item);
    }
    
    // Mutable iteration
    for item in &mut v {    // iterate with &mut Vec
        *item *= 2;
        println!("{}", item);
    }
    println!("{:?}", v);

    // Taking ownership
    for item in v {
        println!("{}", item);
    }
    //println!("{:?}", v);  // ERROR! v is now invalid
}

Note: It is instructive to create a Rust project and put this mode in main.rs then look at it in VSCode with the Rust Analyzer extension. Note the datatype decorations that VSCode places next to the variables.

Note #2: The println! macro is pretty flexible in the types of arguments it can take. In the example above, we are passing it a &i32, a &mut i32, and a i32.

Key Takeaways

  • Stack: fixed-size, fast; Heap: dynamic, flexible
  • Ownership ensures memory safety without garbage collection
  • Move semantics prevent double-free
  • Borrowing allows temporary access without ownership transfer
  • One mutable reference XOR many immutable references
  • References must be valid (no dangling pointers)
  • Compiler enforces these rules at compile time

Best Practices

  1. Prefer borrowing over ownership transfer when possible
  2. Use immutable references by default
  3. Keep mutable reference scope minimal
  4. Let the compiler guide you with error messages
  5. Clone only when necessary (performance cost)
  6. Understand whether functions need ownership or just access

In-Class Exercise (10 minutes)

Challenge: Fix the Broken Code

The following code has several ownership and borrowing errors. Your task is to fix them so the code compiles and runs correctly.

I'll call on volunteers to present their solutions.

fn main() {
    let mut numbers = vec![1, 2, 3, 4, 5];
    
    // Task 1: Calculate sum without taking ownership
    let total = calculate_sum(numbers);
    
    // Task 2: Double each number in the vector
    double_values(numbers);
    
    // Task 3: Print both the original and doubled values
    println!("Original sum: {}", total);
    println!("Doubled values: {:?}", numbers);
    
    // Task 4: Add new numbers to the vector
    add_numbers(numbers, vec![6, 7, 8]);
    println!("After adding: {:?}", numbers);
}

fn calculate_sum(v: Vec<i32>) -> i32 {
    let mut sum = 0;
    for num in v {
        sum += num;
    }
    sum
}

fn double_values(v: Vec<i32>) {
    for num in v {
        num *= 2;
    }
}

fn add_numbers(v: Vec<i32>, new_nums: Vec<i32>) {
    for num in new_nums {
        v.push(num);
    }
}

Hints:

  • Think about which functions need ownership vs borrowing
  • Consider when you need & vs &mut
  • Remember: you can't modify through an immutable reference
  • The original vector should still be usable in main after function calls

Let's Review

Review solutions.