Lifetimes and the Borrow Checker

Article · 15:00

A lifetime is the span of code for which a reference is valid. The compiler works most of them out alone; the annotations you write exist to tell it how several references relate when it cannot tell. With elision, structs that borrow, and static as a bound.

Lifetimes and the Borrow Checker

A lifetime is the span of code for which a reference is valid. The compiler works most of them out on its own; the annotations you write exist to tell it how the lifetimes of several references relate, when it cannot tell.

The rule being enforced is one sentence: a reference must never outlive the value it points at.

fn main() {
    let outer;
    {
        let inner = String::from("scoped");
        outer = inner.len();          // copying the length is fine
        // outer = &inner;            // a reference would not be: inner dies here
    }
    println!("{outer}");

    // The classic: returning a reference to a local.
    //   fn broken() -> &String { let s = String::from("x"); &s }
    // The value is dropped at the end of the function, so the reference would
    // dangle. Return the String itself instead.
    println!("{}", make_owned());

    // Returning a reference INTO an argument is fine: it lives as long as the
    // argument does, and the compiler can see that.
    let text = String::from("hello world");
    println!("{}", first_word(&text));
}

fn make_owned() -> String {
    String::from("owned, so it can leave")
}

fn first_word(text: &str) -> &str {
    text.split_whitespace().next().unwrap_or("")
}

first_word needs no annotation because of lifetime elision: one input reference means the output borrows from it, and the compiler applies that rule silently.

When you have to write one

With two input references the compiler cannot guess which the result borrows from, so you say.

fn main() {
    let long = String::from("a longer string");
    let result;
    {
        let short = String::from("short");
        result = longest(&long, &short);
        println!("{result}");         // fine: both are alive here
    }
    // println!("{result}");          // not fine: `short` has been dropped,
                                      // and 'a is the SHORTER of the two

    // Different lifetimes, when only one of them matters to the result.
    let announcement = String::from("today's winner");
    println!("{}", first_with_note(&long, &announcement));
}

// 'a says: the result lives as long as the shorter of x and y.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

// Here the result borrows only from `text`, so `note` gets its own lifetime -
// and the caller is free to drop it immediately afterwards.
fn first_with_note<'a, 'b>(text: &'a str, note: &'b str) -> &'a str {
    println!("  (note: {note})");
    text.split_whitespace().next().unwrap_or("")
}

Structs that hold references

A struct holding a reference needs a lifetime parameter, and it says: this struct cannot outlive what it borrows.

#[derive(Debug)]
struct Excerpt<'a> {
    part: &'a str,
}

impl<'a> Excerpt<'a> {
    fn new(text: &'a str) -> Self {
        let part = text.split('.').next().unwrap_or(text);
        Excerpt { part }
    }

    // Elision applies here too: the result borrows from &self.
    fn announce(&self, note: &str) -> &str {
        println!("  ({note})");
        self.part
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let excerpt = Excerpt::new(&novel);

    println!("{excerpt:?}");
    println!("{}", excerpt.announce("first sentence"));

    // drop(novel);                   // would not compile: excerpt borrows it
    println!("{}", excerpt.part);
}

This is why holding references in structs is less common in Rust than you might expect: it ties the struct's life to something else's. Owning a String is often simpler, and the copy is rarely the bottleneck.

'static, and what it does not mean

fn main() {
    // A string literal is baked into the binary, so it lives for the whole
    // program: &'static str.
    let literal: &'static str = "I live forever";
    println!("{literal}");

    // 'static as a BOUND means "contains no references that could dangle",
    // which includes every owned value. It does not mean "lives forever".
    print_owned(String::from("owned, and 'static because it borrows nothing"));
    print_owned(42);

    // This is why spawning a thread requires 'static: the thread may outlive
    // the spawner, so it cannot hold a borrow of the spawner's locals.
    let owned = String::from("moved into the thread");
    let handle = std::thread::spawn(move || {
        println!("{owned}");
    });
    handle.join().unwrap();
}

fn print_owned<T: std::fmt::Display + 'static>(value: T) {
    println!("{value}");
}

The borrow checker in practice

Most fights with the borrow checker are one of three patterns, and each has a standard answer.

fn main() {
    // 1. Mutating while iterating. The answer: collect what to do, then do it.
    let mut numbers = vec![1, 2, 3, 4];
    // for n in &numbers { if *n % 2 == 0 { numbers.remove(0); } }    // no
    numbers.retain(|n| n % 2 != 0);
    println!("{numbers:?}");

    // 2. Two mutable borrows of different fields. The answer: destructure, so
    // the compiler can see they are disjoint.
    let mut pair = Pair { left: vec![1], right: vec![2] };
    let Pair { left, right } = &mut pair;
    left.push(9);
    right.push(8);
    println!("{pair:?}");

    // 3. Needing the old value while producing the new one. The answer:
    // std::mem::take or replace, which swaps in the default and hands you
    // ownership of what was there.
    let mut state = State::Loaded(vec![1, 2, 3]);
    let previous = std::mem::replace(&mut state, State::Empty);
    match previous {
        State::Loaded(items) => println!("took ownership of {} items -> {state:?}", items.len()),
        State::Empty => println!("there was nothing to take"),
    }
}

#[derive(Debug)]
struct Pair {
    left: Vec<i32>,
    right: Vec<i32>,
}

#[derive(Debug)]
enum State {
    Empty,
    Loaded(Vec<i32>),
}

What to take away

  • A lifetime is a span, not a duration; the rule is that a reference must not outlive its value.
  • Elision covers the common cases - you annotate when several references relate.
  • <'a> on a function says which input the output borrows from.
  • A struct holding a reference cannot outlive what it borrows; owning is often simpler.
  • 'static as a bound means "borrows nothing", which is why thread::spawn requires it.
  • retain, destructuring, and mem::take/replace resolve most borrow-checker fights.