Lecture 15 - Ownership and Vec
Logistics
- Midterm grades and corrections assignment posted
- HW3 late deadline tonight
We are here
- Last time: Stack and heap
- Today: Rust's ownership system + Vec+Box for heap data
- Next time: Borrowing and references (&, *)
Learning Objectives
By the end of today, you should be able to:
- Explain Rust's three ownership rules
- Understand when data gets moved vs copied
- Draw stack/heap diagrams showing ownership
- Use
Vec<T>andBox<T>on the heap - Debug common ownership compiler errors
Recall: stack vs heap
Stack: Fast, fixed-size, automatic cleanup
- Like a stack of plates - last in, first out
- Each function call gets its own "stack frame"
- All the simple types you've used:
i32,bool,char, arrays, tuples
Heap: Flexible size, manual management (but Rust helps!)
- Like a warehouse - rent space when you need it
- For data that can grow/shrink or is really big
StringandVec<T>store their actual data here
Key idea: Stack variables can hold pointers (addresses) to heap data
Part 1 - Ownership in Rust
Ownership tracks what variable is responsible for data that is on the heap.
Why ownership:
- It helps efficiently free up memory when it is no longer needed
- It helps prevent "undefined behavior" that arises from less-strict approaches
Rust's Three Ownership Rules
These are the fundamental rules that make Rust memory-safe:
- Each value in Rust has an owner
- There can only be one owner at a time
- When the owner goes out of scope, the value will be dropped
Rule 1: Each value in Rust has an owner
#![allow(unused)] fn main() { let s1 = String::from("hello"); // s1 owns the string // There is exactly ONE owner of the "hello" data on the heap }
Stack/Heap diagram:
Stack: Heap:
┌─────────────┐ ┌─────┬─────┬─────┬─────┬─────┐
│ s1: String │ │ 'h' │ 'e' │ 'l' │ 'l' │ 'o' │
│ ├ ptr ──────┼─▶│ │ │ │ │ │
│ ├ len: 5 │ └─────┴─────┴─────┴─────┴─────┘
│ └ cap: 5 │ ↑
└─────────────┘ Owner: s1
Rule 2: There can only be one owner at a time
fn main(){ let s1 = String::from("hello"); let s2 = s1; // Ownership MOVES from s1 to s2 // println!("{}", s1); // ERROR! s1 no longer owns the data println!("{}", s2); // OK! s2 now owns it }
What happens in memory:
Before move: After move:
Stack: Stack:
┌─────────────┐ ┌─────────────┐
│ s1: String │ │ s2: String │
│ ├ ptr ──────┼──┐ │ ├ ptr ──────┼─┐
│ ├ len: 5 │ │ │ ├ len: 5 │ │
│ └ cap: 5 │ │ │ └ cap: 5 │ │
└─────────────┘ │ ├─────────────┘ │
│ │ s1: ??? │ │
│ └─────────────┘ │
│ │
│ Heap: │
│ ┌─────┬─────┬─────┬─────┬─────┐
└─▶│ 'h' │ 'e' │ 'l' │ 'l' │ 'o' │
└─────┴─────┴─────┴─────┴─────┘
Rule 3: When the owner goes out of scope, the value is dropped
fn main(){ { let s = String::from("hello"); // s owns the string } // s goes out of scope, heap memory is freed automatically! println!("{}", s); }
Move vs Copy: The Key Distinction
Stack data gets copied
#![allow(unused)] fn main() { let x = 5; let y = x; // x is copied to y println!("{} {}", x, y); // Both work! }
Heap data gets moved
#![allow(unused)] fn main() { let s1 = String::from("hello"); let s2 = s1; // s1 is moved to s2 // println!("{}", s1); // s1 is no longer valid println!("{}", s2); // Only s2 works now }
Why would an int get copied but a string get moved by default?
Think-pair-share
Passing data to functions follows the same rules
fn takes_ownership(some_string: String) { println!("in fn: {}", some_string); } // some_string goes out of scope and heap memory is freed fn makes_copy(some_integer: i32) { println!("in fn: {}", some_integer); } // some_integer goes out of scope, but it's just a copy fn main() { let s = String::from("hello"); takes_ownership(s); // s is moved into the function // println!("in main: {}", s); // ERROR! s is no longer valid let x = 5; makes_copy(x); // x is copied into the function println!("in main: {}", x); // OK! x is still valid }
Function return values also transfer ownership
fn gives_ownership() -> String { let some_string = String::from("yours"); some_string // Ownership moves to caller } fn takes_and_gives_back(a_string: String) -> String { a_string // Ownership moves back to caller } fn main() { let s1 = gives_ownership(); // Ownership moves from function to s1 let s2 = String::from("hello"); // s2 comes into scope let s3 = takes_and_gives_back(s2); // s2 moves in, s3 gets it back // s2 is no longer valid, but s1 and s3 are println!("s1: {}", s1); // println!("s2: {}", s2); println!("s3: {}", s3); }
Let's draw these moves on the board.
Try to predict what happens before running:
Think-pair-share
fn main() { let s1 = String::from("world"); let s2 = process_string(s1); println!("{}", s2); // Will this work? println!("{}", s1); // Will this work? } fn process_string(input: String) -> String { format!("Hello, {}!", input) }
Part 2 - Vec and Box on the heap
Finally! I know some of you have been using Vec anyway because Rust's arrays are so limiting...
Now we'll get to start using Vec for real.
Why arrays were such a pain
- They live on the stack, so... they're fixed size and size must be known at compile time
What is a Vec
- Contains a single type, like an array
- Can change sizes!
- Lives on the heap
Creating Vec
#![allow(unused)] fn main() { // Three ways to create a Vec: let mut numbers = Vec::new(); // Empty vector let mut scores = vec![85, 92, 78]; // With initial data let mut names: Vec<String> = Vec::new(); // Empty with type annotation }
Vec in memory (let's trace vec![85, 92, 78]):
Stack: Heap:
┌─────────────────┐ ┌────┬────┬────┬────┐
│ scores: Vec<i32>│ │ 85 │ 92 │ 78 │ ?? │
│ ├ ptr ──────────┼─────▶│ │ │ │ │
│ ├ len: 3 │ └────┴────┴────┴────┘
│ └ cap: 4 │ capacity = 4, length = 3
└─────────────────┘
Basic Vec Operations
fn main() { let mut numbers = vec![1, 2, 3]; // Add elements (might reallocate!) numbers.push(4); numbers.push(5); // Other basic operations numbers.pop(); let num_len = numbers.len(); // Access elements (copies the value!) let first = numbers[0]; let third = numbers[2]; println!("First: {}, Third: {}", first, third); println!("Vec: {:?}", numbers); }
Let's draw it on the board!
Key insight: numbers[0] copies the value from heap to stack because i32 is a "copy type".
Capacity vs Length
#![allow(unused)] fn main() { let mut vec = Vec::with_capacity(4); // Reserve space for 4 elements println!("Length: {}, Capacity: {}", vec.len(), vec.capacity()); vec.push(1); vec.push(2); println!("Length: {}, Capacity: {}", vec.len(), vec.capacity()); vec.push(3); vec.push(4); vec.push(5); // This might cause reallocation! println!("Length: {}, Capacity: {}", vec.len(), vec.capacity()); }
When Vec grows beyond capacity: Rust allocates a bigger chunk of heap memory, copies all data over, and frees the old chunk. You don't have to worry about this!
Vec Ownership in Action
Moving Vec to functions:
fn main() { let my_vec = vec![1, 2, 3]; let result = process_numbers(my_vec); // my_vec moves into function // println!("{:?}", my_vec); // ERROR! my_vec no longer valid println!("{:?}", result); // OK! result owns the data now } fn process_numbers(mut numbers: Vec<i32>) -> Vec<i32> { numbers.push(99); numbers }
Copying values FROM Vec:
fn main() { let numbers = vec![10, 20, 30, 40]; // These copy values from heap to stack: let first = numbers[0]; // first = 10 (copied) let second = numbers[1]; // second = 20 (copied) // Original Vec still owns the heap data: println!("Vec still works: {:?}", numbers); println!("Copied values: {}, {}", first, second); }
Stack/heap after copying:
Stack: Heap:
┌─────────────────┐
│ second: 20 │
├─────────────────┤
│ first: 10 │
├─────────────────┤ ┌────┬────┬────┬────┐
│ numbers: Vec │ │ 10 │ 20 │ 30 │ 40 │
│ ├ ptr ──────────┼─────▶│ │ │ │ │
│ ├ len: 4 │ └────┴────┴────┴────┘
│ └ cap: 4 │
└─────────────────┘
What About Vec<String>?
Important difference: Vec<String> contains heap data inside heap data!
fn main(){ let mut names = vec![ String::from("Alice"), String::from("Bob") ]; // This WON'T work the same way: // let first_name = names[0]; // Can't copy String! }
Stack/heap with Vec
Stack: Heap (Vec data): Heap (String data):
┌────────────────┐ ┌─────────────────┐ ┌─────┬─────┬─────┬─────┬─────┐
│ names: Vec │ │ String("Alice") │───────▶│ 'A' │ 'l' │ 'i' │ 'c' │ 'e' │
│ ├ ptr ─────────┼────▶│ ├ ptr ──────────┼────────┤ │ │ │ │ │
│ ├ len: 2 │ │ ├ len: 5 │ └─────┴─────┴─────┴─────┴─────┘
│ └ cap: 2 │ │ └ cap: 5 │
└────────────────┘ ├─────────────────┤ ┌─────┬─────┬─────┐
│ String("Bob") │───────▶│ 'B' │ 'o' │ 'b' │
│ ├ ptr ──────────┼────────┤ │ │ │
│ ├ len: 3 │ └─────┴─────┴─────┘
│ └ cap: 3 │
└─────────────────┘
Why? String doesn't implement Copy - we'd be copying heap pointers, which violates ownership rules.
We'll learn how to handle this next lecture with borrowing!
Box - for when your stack data is REALLY BIG
Sometimes data that would usually go on the stack is just too big:
#![allow(unused)] fn main() { // This is fine - small array let small_data = [0; 1000]; // This might crash your program - too big for the stack! // let huge_data = [0; 10_000_000]; }
So we create a Box to force it onto the heap:
Box: Moving big data to the warehouse
#![allow(unused)] fn main() { let huge_data = Box::new([0; 10_000_000]); println!("Successfully created {} numbers", huge_data.len()); }
You can actually put practically anything in a box! We'll discuss them more later, but for now, they're just another tool for us to think about stack/heap and ownership.
When do you need a box?
- Large datasets: Millions of records
- Big matrices: Large 2D arrays for data analysis
- Deep structures: Complex nested data
Just for fun - nesting things in boxes (Yes, even boxes in boxes!)
You can put complex heap-allocated types inside a Box:
// Box containing a Vec of Strings (heap in heap in heap!) let boxed_names = Box::new(vec![ String::from("Alice"), String::from("Bob"), String::from("Charlie") ]); println!("Names in box: {:?}", boxed_names); // Box in a Box (why not?) let box_in_box = Box::new(Box::new(42)); println!("Deeply boxed value: {}", box_in_box); // Box containing a Vec of Boxes (getting silly now!) let boxes_in_vec_in_box = Box::new(vec![ Box::new(1), Box::new(2), Box::new(3) ]); println!("Box containing Vec of Boxes: {:?}", boxes_in_vec_in_box);
What's happening here?
boxed_names: Stack has a Box pointer -> Heap has Vec metadata -> Heap has String pointers -> Heap has actual string databox_in_box: Stack has a Box pointer -> Heap has another Box pointer -> Heap has the number 42boxes_in_vec_in_box: Stack has a Box pointer -> Heap has Vec metadata -> Heap has Box pointers -> Heap has the actual numbers
Activity time before Part 3!
Act 1: Copy vs Move (6 students)
fn main() { let x = 5; let y = x; let s1 = String::from("hello"); let s2 = s1; println!("{} {}", x, y); // println!("{} {}", s1, s2); }
Act 2: Function calls and returning ownership (4 students)
fn main() { let data = vec![1, 2, 3]; let data = process(data); println!("{:?}", data); // Works! } fn process(mut numbers: Vec<i32>) -> Vec<i32> { numbers.push(4); numbers }
Act 3: Attack of the Clones (8 students)
fn main() { let s1 = String::from("hello"); let s2 = s1.clone(); println!("{} {}", s1, s2); let s3 = s1; let s4 = s2; println!("{} {}", s3, s4); let names = vec![s3, s4]; }
Finale: The Box Office (14 students!!)
fn main() { let ticket_number = 42; let venue = String::from("Stage"); let guest_list = vec![ String::from("Alice"), String::from("Bob") ]; // Box in a Box! let vip_box = Box::new(Box::new(String::from("VIP"))); let show = prepare_show(guest_list, vip_box); println!("Show at {} with ticket {}", venue, ticket_number); println!("Final show: {:?}", show); } fn prepare_show(mut guests: Vec<String>, special: Box<Box<String>>) -> Box<Vec<String>> { guests.push(String::from("Charlie")); guests.push(*special); // Unbox twice! Box::new(guests) }
If you weren't selected, please leave a note on stage or email me after class so I can track you were here!
Part 3: Debugging Ownership Errors
Let's practice fixing common ownership errors you'll encounter:
Error 1: Use After Move
This code won't compile:
fn main() { let data = vec![1, 2, 3]; process_data(data); println!("{:?}", data); // ERROR! } fn process_data(vec: Vec<i32>) { println!("Processing: {:?}", vec); }
Compiler error: "borrow of moved value: data"
Fix option 1: Return the data from the function
fn main() { let data = vec![1, 2, 3]; let data = process_data(data); // Get it back! println!("{:?}", data); // OK! } fn process_data(vec: Vec<i32>) -> Vec<i32> { println!("Processing: {:?}", vec); vec // Return ownership }
Fix option 2: Clone the data (makes a copy)
fn main() { let data = vec![1, 2, 3]; process_data(data.clone()); // Send a copy println!("{:?}", data); // OK! } fn process_data(vec: Vec<i32>) { println!("Processing: {:?}", vec); }
Error 2: Multiple Moves
This code won't compile:
fn main() { let message = String::from("Hello"); let a = message; let b = message; // ERROR! Can't move twice println!("{} {}", a, b); }
Compiler error: "use of moved value: message"
Fix: Clone for multiple copies
fn main() { let message = String::from("Hello"); let a = message.clone(); // Make a copy let b = message; // Move original println!("{} {}", a, b); // Both work! }
Error 3: Trying to Copy Non-Copy Types
This code won't compile:
fn main() { let names = vec![String::from("Alice"), String::from("Bob")]; let first = names[0]; // ERROR! Can't copy String println!("{}", first); }
Compiler error: "cannot move out of index of Vec<String>"
Fix: Clone the specific element
fn main() { let names = vec![String::from("Alice"), String::from("Bob")]; let first = names[0].clone(); // Clone just this element println!("{}", first); // Works! println!("{:?}", names); // Original Vec still works! }