Generics, Traits and Lifetimes · about 45 min

Lifetimes: Naming How Long Borrows Live

Why this matters

In module 5 you learned that a reference must never outlive the value it points to, and that the compiler works this out within a function. At function boundaries and inside structs it sometimes needs your help: when a function returns a reference, which input does it point into? When a struct holds a reference, how long must the referenced data live? Lifetime annotations answer those questions. They are the most feared part of Rust syntax, but the idea is small: they name relationships between borrows so the compiler can check them. They never change how long anything lives.

The problem

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

fn main() {
    println!("{}", longest("hello", "hi"));
}
error[E0106]: missing lifetime specifier
 --> main.rs:1:33
  |
1 | fn longest(a: &str, b: &str) -> &str {
  |               ----     ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `a` or `b`
help: consider introducing a named lifetime parameter
  |
1 | fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

The function returns a reference into either a or b, depending on run-time data. A caller needs to know how long the result stays valid, and the borrow checker needs to check the caller without reading the function body (it checks each function on its own, against its signature). So the signature must state the relationship.

Lifetime parameters

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

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

So at the call site, the result may be used only while both arguments are still valid:

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

fn main() {
    let s1 = String::from("a long string");
    let result;
    {
        let s2 = String::from("short");
        result = longest(&s1, &s2);
    }
    println!("outside: {result}");
}
error[E0597]: `s2` does not live long enough
  --> main.rs:10:31
   |
9  |         let s2 = String::from("short");
   |             -- binding `s2` declared here
10 |         result = longest(&s1, &s2);
   |                               ^^^ borrowed value does not live long enough
11 |     }
   |     - `s2` dropped here while still borrowed
12 |     println!("outside: {result}");
   |                        -------- borrow later used here

You can see that result actually points at s1, which is still alive. But the signature only promises "valid while both are", and the compiler checks callers against the signature, not the body. That is a feature: the function could change to return b tomorrow without breaking any caller's safety.

Annotations describe, they do not prescribe

A lifetime annotation never extends or shortens how long a value lives. Values live until their owner's scope ends, as always. Annotations only relate lifetimes so the compiler can verify that the code is consistent. If the body returned a reference to a local, no annotation could make it valid (module 5's E0515).

Not every parameter needs the same lifetime. If the result only comes from the first input, say so:

fn before_colon<'a>(text: &'a str, _sep_hint: &str) -> &'a str {
    match text.find(':') {
        Some(i) => &text[..i],
        None => text,
    }
}

fn main() {
    let line = String::from("key: value");
    let k;
    {
        let hint = String::from(":");
        k = before_colon(&line, &hint);
    }
    println!("{k}");
}
key

hint may die early because the result is tied only to text.

Elision: why you rarely write them

Most functions with references need no annotations, because the compiler applies 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 references.
  3. If there is a &self or &mut self parameter, its lifetime is assigned to all output references.

So fn first_word(s: &str) -> &str is understood as fn first_word<'a>(s: &'a str) -> &'a str, and a method fn name(&self) -> &str returns something borrowed from self. You write annotations only when the rules do not determine the answer: typically two or more reference inputs and a reference output, no self.

Structs that hold references

A struct can store a reference, but then the struct must not outlive the data it borrows. The lifetime becomes a parameter of the type:

struct Excerpt<'a> {
    text: &'a str,
}

impl<'a> Excerpt<'a> {
    fn first_sentence(&self) -> &'a str {
        self.text.split('.').next().unwrap_or("")
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let e = Excerpt { text: &novel };
    println!("{}", e.first_sentence());
}
Call me Ishmael

Excerpt<'a> reads "an Excerpt that borrows something for 'a". The compiler ensures no Excerpt outlives novel. Such structs are views: cheap to create, no copying, ideal for parsers and iterators over borrowed data. When a struct should be independent of where its data came from, store owned data (String) instead, which is why owned fields are the default advice.

'static

'static is the lifetime of the whole program. String literals are &'static str, since they are stored in the executable. A bound T: 'static means "T contains no non-static references" (it owns all its data), which you will meet with threads in module 14, since a spawned thread may outlive the function that started it.

Resist the temptation to fix lifetime errors by writing 'static everywhere: it demands that the data live forever, which local data cannot.

Common mistakes

Recap

Going further: the Rust Book chapter 10.3, "Validating References with Lifetimes", walks through the same ideas with diagrams. Try writing fn split_pair<'a>(s: &'a str) -> (&'a str, &'a str) and calling it on a String.

Exercises

Exercise 1 · write code

Annotate longest

longest returns whichever of its two string slices is longer (the first if equal). It fails with E0106 because the compiler cannot tell which input the result borrows from. Add a lifetime parameter so it compiles. The given main reads two lines and prints:

longest: elephant

for cat and elephant.

File: main.rs

Starting point

use std::io;

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

fn main() {
    let mut x = String::new();
    io::stdin().read_line(&mut x).unwrap();
    let mut y = String::new();
    io::stdin().read_line(&mut y).unwrap();
    println!("longest: {}", longest(x.trim(), y.trim()));
}

Example

input

cat
elephant

expected output

longest: elephant

Example

input

same
size

expected output

longest: same
Hint 1

Declare the lifetime after the name: fn longest<'a>(...).

Hint 2

Use it on both parameters and the return type: a: &'a str, b: &'a str) -> &'a str.

Reference solution
use std::io;

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

fn main() {
    let mut x = String::new();
    io::stdin().read_line(&mut x).unwrap();
    let mut y = String::new();
    io::stdin().read_line(&mut y).unwrap();
    println!("longest: {}", longest(x.trim(), y.trim()));
}

Exercise 2 · write code

A struct that borrows

Define struct Highlight<'a> { text: &'a str, start: usize, end: usize } with a method fn part(&self) -> &'a str returning &self.text[self.start..self.end]. Write fn find_word<'a>(text: &'a str, word: &str) -> Option<Highlight<'a>> returning a highlight of the first occurrence of word in text (use text.find(word)).

The given main reads a text line and a word line and prints found "WORD" at START..END or not found:

found "fox" at 16..19

for the quick brown fox and fox.

File: main.rs

Starting point

use std::io;

fn main() {
    let mut text = String::new();
    io::stdin().read_line(&mut text).unwrap();
    let mut word = String::new();
    io::stdin().read_line(&mut word).unwrap();
    match find_word(text.trim(), word.trim()) {
        Some(h) => println!("found {:?} at {}..{}", h.part(), h.start, h.end),
        None => println!("not found"),
    }
}

Example

input

the quick brown fox
fox

expected output

found "fox" at 16..19

Example

input

hello
bye

expected output

not found
Hint 1

The impl needs the lifetime too: impl<'a> Highlight<'a> { ... }.

Hint 2

let start = text.find(word)?; then build the struct with end: start + word.len().

Reference solution
use std::io;

struct Highlight<'a> {
    text: &'a str,
    start: usize,
    end: usize,
}

impl<'a> Highlight<'a> {
    fn part(&self) -> &'a str {
        &self.text[self.start..self.end]
    }
}

fn find_word<'a>(text: &'a str, word: &str) -> Option<Highlight<'a>> {
    let start = text.find(word)?;
    Some(Highlight { text, start, end: start + word.len() })
}

fn main() {
    let mut text = String::new();
    io::stdin().read_line(&mut text).unwrap();
    let mut word = String::new();
    io::stdin().read_line(&mut word).unwrap();
    match find_word(text.trim(), word.trim()) {
        Some(h) => println!("found {:?} at {}..{}", h.part(), h.start, h.end),
        None => println!("not found"),
    }
}

Exercise 3 · quiz

Which need annotations?

Which of these signatures compile without writing any lifetime annotations? Select all that apply.

Answer
  • fn trim_dots(s: &str) -> &str

  • fn name(&self) -> &str (a method)

  • fn first<'a>(v: &'a [i32]) -> &'a i32 written as fn first(v: &[i32]) -> &i32

Elision rules: each reference parameter gets its own lifetime; if there is exactly one input lifetime, outputs get it; in methods, outputs get the lifetime of &self. So (a) and (d) have one input, (c) is a method, and all three are inferred. (b) has two reference inputs and no self, so the compiler cannot know which one the result borrows from and asks you to say it.