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> and Box<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
  • String and Vec<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:

  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 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 (heap data pointing to more heap data!):

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 data
  • box_in_box: Stack has a Box pointer -> Heap has another Box pointer -> Heap has the number 42
  • boxes_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!

Random student generator

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!
}