RustArena

Rust lifetimes explained

What lifetimes are in Rust, why the borrow checker needs them, how to read and write 'a annotations, the elision rules, and structs that hold references.

A lifetime is the stretch of code during which a reference is valid. Every reference has one. Most of the time the compiler works lifetimes out by itself and you never write them; you only add lifetime annotations when a function or struct connects several references and the compiler can't tell how they relate.

The problem lifetimes solve

fn main() {
    let r;
    {
        let x = 5;
        r = &x;
    }
    println!("{r}");
}
error[E0597]: `x` does not live long enough

x is dropped at the inner }, but r is used after that. The borrow checker compares the two regions and rejects the program. In C this would compile and read freed memory.

Annotations describe relationships

Within one function the compiler can see everything. Across a function call it only sees the signature. Consider:

fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() >= b.len() { a } else { b }
}

'a (read "lifetime a") is a lifetime parameter, declared in <> like a generic type. The signature says: "the returned reference is valid for as long as both a and b are". Callers are then checked against that promise:

fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() >= b.len() { a } else { b }
}

fn main() {
    let outer = String::from("a long string");
    let result;
    {
        let inner = String::from("short");
        result = longest(&outer, &inner);
    }
    println!("{result}");
}
error[E0597]: `inner` does not live long enough

Annotations never make anything live longer. They are a contract that lets the compiler check each function on its own, and each call on its own.

Lifetime elision

You rarely write lifetimes because of three elision rules:

  1. Each reference parameter gets its own lifetime.
  2. If there is exactly one input lifetime, it's used for all outputs: fn first_word(s: &str) -> &str needs nothing.
  3. In methods, if there's &self or &mut self, outputs get self's lifetime: fn name(&self) -> &str needs nothing.

When the rules can't decide (two reference inputs, a reference output, no self), you get E0106 "missing lifetime specifier" and must annotate.

Structs that hold references

A struct with a reference field needs a lifetime parameter, meaning "a Parser can't outlive the text it reads":

struct Parser<'a> {
    input: &'a str,
    pos: usize,
}

This is common for short-lived views (parsers, iterators, tokenizers) and avoids copying. For long-lived data (config, records, anything stored in a collection or sent to a thread), make the struct own its data (String, Vec<T>) instead.

'static

'static is the lifetime of the whole program. String literals are &'static str. A bound T: 'static (required by thread::spawn) means "contains no short-lived references", and owned types like String satisfy it.

Practical advice

Example

fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() >= b.len() { a } else { b }
}

struct Parser<'a> {
    input: &'a str,
    pos: usize,
}

impl<'a> Parser<'a> {
    fn new(input: &'a str) -> Self {
        Parser { input, pos: 0 }
    }

    // The returned word borrows from the input text, not from the parser.
    fn next_word(&mut self) -> Option<&'a str> {
        let rest = &self.input[self.pos..];
        let rest_trimmed = rest.trim_start();
        if rest_trimmed.is_empty() {
            return None;
        }
        let start = self.pos + (rest.len() - rest_trimmed.len());
        let end = rest_trimmed.find(' ').map(|i| start + i).unwrap_or(self.input.len());
        self.pos = end;
        Some(&self.input[start..end])
    }
}

fn main() {
    let outer = String::from("a long string");
    {
        let inner = String::from("short");
        let result = longest(&outer, &inner);
        println!("longest: {result}");
    }

    let text = String::from("  move  north then attack ");
    let mut words = Vec::new();
    {
        let mut p = Parser::new(&text);
        while let Some(w) = p.next_word() {
            words.push(w);
        }
    } // the parser is gone, but the words still borrow `text`
    println!("{words:?}");
}

Output:

longest: a long string
["move", "north", "then", "attack"]

Practice it