Skip to content
AgileLogics
8 min read

Advanced Lifetimes

Prerequisites: Advanced traits

Lifetime annotations never change how long a value lives. They describe relationships between borrows so the compiler can check them. This lesson makes those relationships precise.

The elision rules, exactly

You rarely write lifetimes because the compiler applies three rules to function signatures:

  1. Every elided lifetime in an input gets its own distinct parameter. fn f(a: &str, b: &str) means fn f<'a, 'b>(a: &'a str, b: &'b str).
  2. If there is exactly one input lifetime, it is assigned to every elided output lifetime. fn trim(s: &str) -> &str ties the result to s.
  3. If one input is &self or &mut self, its lifetime is assigned to every elided output lifetime.

If none of these settles an output lifetime, you must write it:

fn pick(a: &str, b: &str) -> &str {
    if a.len() > b.len() { a } else { b }
}
error[E0106]: missing lifetime specifier

The fix states the relationship: fn pick<'a>(a: &'a str, b: &'a str) -> &'a str.

Multiple lifetimes

A single 'a everywhere is the easy answer but can be too strict. If the result only ever borrows from one argument, say so, and callers get more freedom with the others:

fn find_after<'t>(text: &'t str, marker: &str) -> Option<&'t str> {
    text.find(marker).map(|i| &text[i + marker.len()..])
}

fn main() {
    let text = String::from("user=alice");
    let value = {
        let marker = String::from("user="); // dropped at the end of this block
        find_after(&text, &marker)
    };
    println!("{value:?}");
}

Expected output:

Some("alice")

marker gets its own elided lifetime (rule 1), and the output is tied only to 't, so value may outlive marker.

Lifetimes on structs and impl blocks

A struct holding a reference needs a lifetime parameter, and its impl block declares it again. The interesting choice is what methods return:

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

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

    // Returns &'a str: borrowed from the input, NOT from the parser.
    fn next_word(&mut self) -> Option<&'a str> {
        let rest = &self.input[self.pos..];
        let trimmed = rest.trim_start();
        if trimmed.is_empty() {
            return None;
        }
        let start = self.pos + (rest.len() - trimmed.len());
        let end = trimmed.find(' ').map_or(self.input.len(), |i| start + i);
        self.pos = end;
        Some(&self.input[start..end])
    }
}

fn main() {
    let text = String::from("let x = 42");
    let mut p = Parser::new(&text);
    let first = p.next_word();
    let second = p.next_word();
    println!("{first:?} {second:?}");
    let rest: Vec<&str> = std::iter::from_fn(|| p.next_word()).collect();
    println!("{rest:?}");
}

Expected output:

Some("let") Some("x")
["=", "42"]

Had the signature been fn next_word(&mut self) -> Option<&str>, rule 3 would tie each word to the &mut self borrow, and holding first while calling next_word again fails:

error[E0499]: cannot borrow `p` as mutable more than once at a time

'static and T: 'static

&'static str is a reference valid for the whole program — string literals are the common case. The bound T: 'static means something different: T contains no borrows shorter than 'static. Every owned type (String, Vec<i32>, Arc<Mutex<_>>) satisfies it. It does not mean the value lives forever.

std::thread::spawn requires 'static because the new thread could outlive the caller's stack:

use std::fmt::Debug;
use std::thread;

fn log_in_background<T: Debug + Send + 'static>(value: T) -> thread::JoinHandle<()> {
    thread::spawn(move || println!("{value:?}"))
}

fn main() {
    log_in_background(vec![1, 2, 3]).join().unwrap(); // owned: fine
    log_in_background("literal").join().unwrap();     // &'static str: fine
}

Expected output:

[1, 2, 3]
"literal"

Passing a borrow of a local String named local instead fails:

error[E0597]: `local` does not live long enough

The fix is to move an owned value in, or to use thread::scope.

Higher-ranked trait bounds (briefly)

Sometimes a bound must hold for every lifetime, not one chosen by the caller. A function that calls a closure on its own local strings is the example: no caller-supplied lifetime could name those locals. for<'a> expresses this:

fn lengths<F>(f: F) -> (usize, usize)
where
    F: for<'a> Fn(&'a str) -> &'a str,
{
    let a = String::from("hello world");
    let b = String::from("rust");
    (f(&a).len(), f(&b).len())
}

fn first_word(s: &str) -> &str {
    s.split(' ').next().unwrap_or("")
}

fn main() {
    println!("{:?}", lengths(first_word));
}

This prints (5, 4). You seldom write for<'a> yourself: F: Fn(&str) -> &str is sugar for exactly this bound. It appears explicitly in trait bounds such as where for<'a> &'a T: IntoIterator.

Common mistakes

  • Adding 'static to make an error disappear. &'static forces callers to pass leaked or literal data; usually you need an owned type instead.
  • Returning a reference to a local: error[E0515]: cannot return reference to local variable. Return the owned value (String, Vec<T>).
  • Tying every reference to one 'a when outputs borrow from just one input.
  • Storing references in long-lived structs (caches, app state). Owned data or Arc is usually simpler than threading a lifetime through everything.

Practice

Write struct Longest<'a> { best: &'a str } with offer(&mut self, s: &'a str) that keeps the longer string, and get(&self) -> &'a str. Show that the result of get can still be used after the Longest value is dropped.

Show solution
struct Longest<'a> {
    best: &'a str,
}

impl<'a> Longest<'a> {
    fn offer(&mut self, s: &'a str) {
        if s.len() > self.best.len() {
            self.best = s;
        }
    }

    // Explicit 'a: the result borrows the strings, not the struct.
    fn get(&self) -> &'a str {
        self.best
    }
}

fn main() {
    let words = vec![String::from("ox"), String::from("zebra"), String::from("cat")];
    let winner = {
        let mut l = Longest { best: "" };
        for w in &words {
            l.offer(w);
        }
        l.get()
    }; // `l` is dropped here; `winner` still borrows `words`
    println!("{winner}");
}

This prints zebra.