Ownership and Lifetimes

This is the chapter that makes Rust click. Ownership is how Rust manages memory without a garbage collector and without manual free. Lifetimes are how the compiler proves your references are always valid. Together, they replace the entire class of memory bugs that plague C programs.

The Three Rules of Ownership

  1. Every value has exactly one owner.
  2. When the owner goes out of scope, the value is dropped (freed).
  3. Ownership can be transferred (moved), not duplicated (by default).
// ownership_basic.rs
fn main() {
    let s1 = String::from("hello");  // s1 owns the String
    let s2 = s1;                     // ownership moves to s2
    // println!("{}", s1);           // ERROR: s1 no longer valid
    println!("{}", s2);              // OK: s2 is the owner now
}
  Before move:
  Stack                  Heap
  +------+---------+    +---+---+---+---+---+
  |  s1  | ptr   --|----| h | e | l | l | o |
  |      | len=5   |    +---+---+---+---+---+
  |      | cap=5   |
  +------+---------+

  After move (s1 = s2):
  Stack                  Heap
  +------+---------+
  |  s1  | invalid |    (no longer accessible)
  +------+---------+
  +------+---------+    +---+---+---+---+---+
  |  s2  | ptr   --|----| h | e | l | l | o |
  |      | len=5   |    +---+---+---+---+---+
  |      | cap=5   |
  +------+---------+

There is still only one pointer to the heap data. When s2 goes out of scope, the heap memory is freed exactly once.

Compare to C, where this "just works" but is dangerous:

/* ownership_c.c -- C has no concept of ownership */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    char *s1 = malloc(6);
    strcpy(s1, "hello");

    char *s2 = s1;  /* both pointers to same memory */

    free(s1);
    /* s2 is now dangling -- use-after-free if accessed */
    /* free(s2); */  /* double-free if called */

    return 0;
}

Rust Note: C trusts the programmer to track who "owns" each allocation mentally. Rust makes ownership explicit in the type system and enforces it at compile time. There is zero runtime cost.

Move Semantics

Passing a value to a function moves it. The caller loses access.

// move_fn.rs
fn take_ownership(s: String) {
    println!("got: {}", s);
}  // s is dropped here

fn main() {
    let s = String::from("hello");
    take_ownership(s);
    // println!("{}", s);  // ERROR: value moved
}

To let the function use the value without taking ownership, pass a reference (borrowing, covered in Chapter 7).

// borrow_fn.rs
fn borrow(s: &String) {
    println!("borrowed: {}", s);
}  // nothing dropped -- we only had a reference

fn main() {
    let s = String::from("hello");
    borrow(&s);
    println!("still mine: {}", s);  // OK
}

Copy vs Clone

Copy Types

Simple, stack-only types implement the Copy trait. Assignment copies the bits instead of moving.

// copy_demo.rs
fn main() {
    let x: i32 = 42;
    let y = x;      // copy, not move -- i32 implements Copy
    println!("x = {}, y = {}", x, y);  // both valid

    let a: f64 = 3.14;
    let b = a;      // copy
    println!("a = {}, b = {}", a, b);  // both valid
}

Types that implement Copy: all integer types, f32, f64, bool, char, tuples of Copy types, fixed-size arrays of Copy types.

Types that do NOT implement Copy: String, Vec<T>, Box<T>, anything that owns heap memory.

Clone

Clone is explicit duplication. You call .clone() to make a deep copy.

// clone_demo.rs
fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();  // deep copy: new heap allocation

    println!("s1 = {}", s1);  // OK -- s1 still valid
    println!("s2 = {}", s2);
}
  After clone:
  Stack                  Heap
  +------+---------+    +---+---+---+---+---+
  |  s1  | ptr   --|----| h | e | l | l | o |  <-- allocation 1
  |      | len=5   |    +---+---+---+---+---+
  +------+---------+
  +------+---------+    +---+---+---+---+---+
  |  s2  | ptr   --|----| h | e | l | l | o |  <-- allocation 2
  |      | len=5   |    +---+---+---+---+---+
  +------+---------+

  Two separate heap allocations. No aliasing.

Try It: Try to assign a Vec<i32> without cloning. Observe the move error. Then add .clone() and see that both vectors work independently.

Lifetimes: What 'a Means

A lifetime is the scope during which a reference is valid. Usually, the compiler infers lifetimes automatically. When it cannot, you annotate them.

// lifetime_basic.rs
fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() >= s2.len() {
        s1
    } else {
        s2
    }
}

fn main() {
    let s1 = String::from("long string");
    let result;
    {
        let s2 = String::from("hi");
        result = longer(&s1, &s2);
        println!("longer: {}", result);
    }
    // println!("{}", result);  // Would fail if s2's data were used
}

The 'a annotation says: "the returned reference lives at least as long as the shorter of the two input references." This lets the compiler verify that the returned reference does not outlive its data.

  Lifetime visualization:

  fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str
                 ^^            ^^              ^^
                 |             |               |
                 +------- all the same 'a -----+
                 |
                 The returned reference is valid for
                 the INTERSECTION of s1 and s2's lifetimes.

  Timeline:
  |---- s1 valid -----------------------------|
  |         |---- s2 valid ----|              |
  |         |---- 'a ---------|              |
  |         |-- result valid --|              |

Why the Compiler Needs Lifetimes

Without lifetimes, the compiler cannot tell if this function is safe:

// lifetime_needed.rs -- what if there were no annotations?
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &byte) in bytes.iter().enumerate() {
        if byte == b' ' {
            return &s[0..i];
        }
    }
    s
}

fn main() {
    let sentence = String::from("hello world");
    let word = first_word(&sentence);
    println!("first word: {}", word);
}

This compiles without explicit lifetime annotations because of lifetime elision rules. The compiler infers that the output lifetime matches the input. Here are the three elision rules:

  1. Each reference parameter gets its own lifetime.
  2. If there is exactly one input lifetime, it is assigned to all output lifetimes.
  3. If one parameter is &self or &mut self, its lifetime is assigned to outputs.

When these rules are insufficient, you must annotate manually.

Structs with References

If a struct holds a reference, it needs a lifetime parameter.

// struct_lifetime.rs
struct Excerpt<'a> {
    text: &'a str,
}

impl<'a> Excerpt<'a> {
    fn new(text: &'a str) -> Self {
        Excerpt { text }
    }

    fn display(&self) {
        println!("Excerpt: {}", self.text);
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let excerpt = Excerpt::new(&novel[..16]);
    excerpt.display();
}
$ rustc struct_lifetime.rs && ./struct_lifetime
Excerpt: Call me Ishmael.

The lifetime 'a guarantees that the Excerpt cannot outlive the string it references. In C, you would just store a const char * with no such guarantee.

/* struct_lifetime.c -- C version: no safety */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct excerpt {
    const char *text;  /* dangling pointer? who knows */
};

int main(void)
{
    struct excerpt e;
    {
        char *novel = strdup("Call me Ishmael. Some years ago...");
        e.text = novel;
        free(novel);  /* e.text is now dangling */
    }
    /* printf("%s\n", e.text); */  /* undefined behavior */
    return 0;
}

Caution: The C version compiles without warnings. The dangling pointer is invisible to the compiler. Rust rejects the equivalent code outright.

The Borrow Checker in Action

The borrow checker enforces ownership and lifetime rules at compile time. Here is a classic example it catches:

// borrow_checker.rs -- This will NOT compile
fn main() {
    let mut v = vec![1, 2, 3];
    let first = &v[0];   // immutable borrow

    v.push(4);            // mutable borrow (push might reallocate!)

    println!("{}", first); // ERROR: first might be dangling
}
error[E0502]: cannot borrow `v` as mutable because it is also
              borrowed as immutable

Why? push might reallocate the underlying buffer, invalidating first. The borrow checker catches this at compile time. In C, this is a silent bug:

/* borrow_bug.c -- C version: silent bug */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int *arr = malloc(3 * sizeof(int));
    arr[0] = 1; arr[1] = 2; arr[2] = 3;

    int *first = &arr[0];  /* pointer into arr */

    /* realloc might move the buffer */
    int *tmp = realloc(arr, 100 * sizeof(int));
    if (tmp) arr = tmp;

    /* first might point to freed memory now */
    printf("%d\n", *first);  /* undefined behavior */

    free(arr);
    return 0;
}

Rc<T>: Reference-Counted Shared Ownership

Sometimes multiple parts of your program need to own the same data. Rc<T> (Reference Counted) tracks how many owners exist and frees the data when the count reaches zero.

// rc_demo.rs
use std::rc::Rc;

fn main() {
    let a = Rc::new(String::from("shared data"));
    println!("ref count after a: {}", Rc::strong_count(&a));

    let b = Rc::clone(&a);  // increment ref count, not deep copy
    println!("ref count after b: {}", Rc::strong_count(&a));

    {
        let c = Rc::clone(&a);
        println!("ref count after c: {}", Rc::strong_count(&a));
    }  // c dropped, ref count decremented

    println!("ref count after c dropped: {}", Rc::strong_count(&a));
    println!("data: {}", a);
}
$ rustc rc_demo.rs && ./rc_demo
ref count after a: 1
ref count after b: 2
ref count after c: 3
ref count after c dropped: 2
data: shared data
  Rc layout:

  Stack                 Heap (Rc control block + data)
  +---+---------+      +----------------+-------------------+
  | a | ptr   --|----->| strong_count=2 | "shared data"     |
  +---+---------+      | weak_count=0   |                   |
  +---+---------+      +----------------+-------------------+
  | b | ptr   --|------^
  +---+---------+

  When all Rc's drop, strong_count hits 0 -> data freed.

Caution: Rc<T> is single-threaded only. It does not use atomic operations. Using it across threads is a compile error.

Arc<T>: Atomic Reference Counting

Arc<T> is the thread-safe version of Rc<T>. It uses atomic operations to update the reference count.

// arc_demo.rs
use std::sync::Arc;
use std::thread;

fn main() {
    let data = Arc::new(vec![1, 2, 3, 4, 5]);

    let mut handles = vec![];

    for i in 0..3 {
        let data_clone = Arc::clone(&data);
        let handle = thread::spawn(move || {
            let sum: i32 = data_clone.iter().sum();
            println!("thread {}: sum = {}", i, sum);
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("ref count: {}", Arc::strong_count(&data));
}
$ rustc arc_demo.rs && ./arc_demo
thread 0: sum = 15
thread 1: sum = 15
thread 2: sum = 15
ref count: 1

Driver Prep: In Rust-for-Linux, Arc<T> is used for shared device state. The kernel's own struct kref is the C equivalent -- a manual reference counter with explicit kref_get and kref_put calls.

When to Use What

  +------------------+--------------------------------------------+
  | Ownership Model  | Use When                                   |
  +------------------+--------------------------------------------+
  | T (owned)        | Single owner, value moves with assignment  |
  | &T               | Read-only access, no ownership change      |
  | &mut T           | Exclusive read-write access, temporary     |
  | Box<T>           | Single owner, heap allocation needed       |
  | Rc<T>            | Multiple owners, single-threaded           |
  | Arc<T>           | Multiple owners, multi-threaded            |
  | Rc<RefCell<T>>   | Multiple owners + interior mutability (ST) |
  | Arc<Mutex<T>>    | Multiple owners + interior mutability (MT) |
  +------------------+--------------------------------------------+

'static Lifetime

The 'static lifetime means the reference is valid for the entire program duration. String literals have 'static lifetime because they are embedded in the binary.

// static_lifetime.rs
fn get_greeting() -> &'static str {
    "Hello, world!"  // string literal: lives forever
}

fn main() {
    let s = get_greeting();
    println!("{}", s);
}

'static does NOT mean "allocated forever." It means "valid for the rest of the program." Leaked memory is also 'static, but that is usually a bug.

When to Use unsafe

unsafe does not turn off the borrow checker. It unlocks five specific powers:

  1. Dereference raw pointers (*const T, *mut T)
  2. Call unsafe functions
  3. Access mutable statics
  4. Implement unsafe traits
  5. Access fields of union types
// unsafe_demo.rs
fn main() {
    let mut x = 42;

    // Creating raw pointers is safe
    let r1 = &x as *const i32;
    let r2 = &mut x as *mut i32;

    // Dereferencing raw pointers requires unsafe
    unsafe {
        println!("r1 = {}", *r1);
        *r2 = 99;
        println!("r2 = {}", *r2);
    }

    // Calling C functions via FFI
    unsafe {
        let pid = libc_getpid();
        println!("pid = {}", pid);
    }
}

// Declaring an external C function
extern "C" {
    #[link_name = "getpid"]
    fn libc_getpid() -> i32;
}
$ rustc unsafe_demo.rs && ./unsafe_demo
r1 = 42
r2 = 99
pid = 12345

Driver Prep: Rust-for-Linux kernel modules use unsafe at the boundary between Rust and the C kernel API. The goal is to wrap unsafe operations in safe abstractions so that driver authors rarely need unsafe in their own code.

C Trusts the Programmer, Rust Trusts the Compiler

This is the philosophical divide. C gives you full control and assumes you know what you are doing. Rust restricts what you can express and proves correctness at compile time.

/* trust_programmer.c */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int *p = malloc(sizeof(int));
    *p = 42;

    int *alias = p;    /* two pointers, same memory */
    free(p);           /* free through one */
    *alias = 99;       /* use through other -- UB */

    /* C: compiles, runs, "works" until it doesn't */
    return 0;
}
// trust_compiler.rs
fn main() {
    let p = Box::new(42);
    // let alias = p;  // ownership moves, p is invalidated
    // println!("{}", *p);  // compile error: use of moved value

    // Rust: does not compile. Bug caught before it exists.
}

The cost of Rust's approach: a learning curve and occasional fights with the borrow checker. The benefit: entire categories of bugs are impossible.

Knowledge Check

  1. What is the difference between Copy and Clone?
  2. What does the lifetime 'a in fn foo<'a>(x: &'a str) -> &'a str mean?
  3. Why is Rc<T> not safe to use across threads?

Common Pitfalls

  • Cloning everything to avoid the borrow checker -- this works but defeats the purpose. Restructure your code instead.
  • Confusing move in closures with ownership transfer -- move captures variables by value, taking ownership.
  • Forgetting that Rc cycles leak -- use Weak<T> references to break cycles.
  • Overusing 'static -- not every reference needs to live forever. Use the narrowest lifetime that works.
  • Putting lifetimes on everything -- trust the elision rules first. Annotate only when the compiler asks.
  • Using unsafe to bypass the borrow checker -- if the borrow checker rejects your code, you almost certainly have a real bug. unsafe does not fix logic errors.
  • Thinking unsafe means "no rules" -- you still must uphold Rust's safety invariants. unsafe means "I, the programmer, guarantee these invariants hold."