106-lifetime-elision — Lifetime Elision Rules
Tutorial
The Problem
Lifetime annotations are often redundant — the compiler can infer them from context. Rust's three lifetime elision rules specify exactly when annotations can be omitted, making the common cases concise while requiring explicit annotations only for ambiguous cases.
Understanding elision rules helps you read Rust code that lacks annotations and know when you need to add them. It also explains why fn first_word(s: &str) -> &str compiles without any 'a.
🎯 Learning Outcomes
Code Example
// Compiler infers: fn first_word<'a>(s: &'a str) -> &'a str
fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or(s)
}
struct TextBuffer { content: String }
impl TextBuffer {
// Compiler infers: fn get_content<'a>(&'a self) -> &'a str
fn get_content(&self) -> &str { &self.content }
fn get_length(&self) -> usize { self.content.len() }
}Key Differences
&self lifetime rule makes virtually all method return values annotation-free, which is why most Rust struct methods look clean.OCaml Approach
OCaml has no lifetime annotations and no elision rules — all references are managed by the GC with no lifetime tracking:
let first_word s = String.split_on_char ' ' s |> List.hd
(* Two inputs — OCaml makes no distinction *)
let pick_first a _b = a (* GC tracks both; no lifetime concern *)
All OCaml functions return values that the GC will keep alive as long as needed. The programmer never needs to annotate how long a return value borrows from an argument.
Full Source
#![allow(clippy::all)]
// Example 106: Lifetime Elision Rules
//
// Rust has three elision rules that let you skip lifetime annotations
// in the most common cases:
//
// Rule 1: Each input reference gets its own distinct lifetime.
// Rule 2: If there is exactly one input lifetime, every output
// reference gets that same lifetime.
// Rule 3: If one of the inputs is &self or &mut self, every output
// reference gets self's lifetime.
//
// When the rules produce an unambiguous answer you write nothing.
// When they don't, the compiler asks you to be explicit.
// ── Approach 1: single input reference (rule 2 applies) ──────────────
// The compiler expands this to:
// fn first_word<'a>(s: &'a str) -> &'a str
// You write nothing — the relationship is obvious.
pub fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or(s)
}
// Two input references → rule 2 cannot apply (ambiguous source).
// We must spell out which input the output borrows from.
pub fn pick_first<'a>(a: &'a str, _b: &str) -> &'a str {
a
}
// ── Approach 2: method with &self (rule 3 applies) ───────────────────
// The compiler expands `fn get_content(&self) -> &str` to
// fn get_content<'a>(&'a self) -> &'a str
pub struct TextBuffer {
content: String,
}
impl TextBuffer {
pub fn new(content: &str) -> Self {
Self {
content: content.to_owned(),
}
}
// Rule 3: output borrows from self — no annotation needed.
pub fn get_content(&self) -> &str {
&self.content
}
pub fn get_length(&self) -> usize {
self.content.len()
}
// Rule 3 still applies even when we also take another reference.
// The returned slice is guaranteed to live as long as `self`.
pub fn trim_to(&self, max: usize) -> &str {
let end = max.min(self.content.len());
&self.content[..end]
}
}
// ── Approach 3: struct holding a reference (explicit lifetime required)
// Elision rules don't cover struct fields — you must write 'a.
pub struct Excerpt<'a> {
pub text: &'a str,
}
impl<'a> Excerpt<'a> {
pub fn new(text: &'a str) -> Self {
Self { text }
}
// Rule 3: output borrows from self, so the return gets self's lifetime.
pub fn content(&self) -> &str {
self.text
}
}
#[cfg(test)]
mod tests {
use super::*;
// ── first_word ────────────────────────────────────────────────────
#[test]
fn test_first_word_with_space() {
assert_eq!(first_word("hello world"), "hello");
}
#[test]
fn test_first_word_single_word() {
assert_eq!(first_word("hello"), "hello");
}
#[test]
fn test_first_word_empty() {
assert_eq!(first_word(""), "");
}
#[test]
fn test_first_word_multiple_words() {
assert_eq!(first_word("one two three"), "one");
}
// ── pick_first (explicit lifetime) ───────────────────────────────
#[test]
fn test_pick_first_returns_a() {
let a = String::from("abcde");
let result;
{
let b = String::from("xy");
// result borrows from `a` (lifetime 'a), so b can end here.
result = pick_first(&a, &b);
}
assert_eq!(result, "abcde");
}
// ── TextBuffer ────────────────────────────────────────────────────
#[test]
fn test_text_buffer_get_content() {
let buf = TextBuffer::new("Hello, World!");
assert_eq!(buf.get_content(), "Hello, World!");
}
#[test]
fn test_text_buffer_get_length() {
let buf = TextBuffer::new("Rust");
assert_eq!(buf.get_length(), 4);
}
#[test]
fn test_text_buffer_trim_to() {
let buf = TextBuffer::new("lifetime elision");
assert_eq!(buf.trim_to(8), "lifetime");
}
#[test]
fn test_text_buffer_trim_to_beyond_length() {
let buf = TextBuffer::new("short");
assert_eq!(buf.trim_to(100), "short");
}
// ── Excerpt ───────────────────────────────────────────────────────
#[test]
fn test_excerpt_content() {
let text = String::from("We choose to go to the Moon.");
let ex = Excerpt::new(&text);
assert_eq!(ex.content(), "We choose to go to the Moon.");
}
#[test]
fn test_excerpt_text_field() {
let s = "four score and seven years";
let ex = Excerpt::new(s);
assert_eq!(ex.text, s);
}
}#[cfg(test)]
mod tests {
use super::*;
// ── first_word ────────────────────────────────────────────────────
#[test]
fn test_first_word_with_space() {
assert_eq!(first_word("hello world"), "hello");
}
#[test]
fn test_first_word_single_word() {
assert_eq!(first_word("hello"), "hello");
}
#[test]
fn test_first_word_empty() {
assert_eq!(first_word(""), "");
}
#[test]
fn test_first_word_multiple_words() {
assert_eq!(first_word("one two three"), "one");
}
// ── pick_first (explicit lifetime) ───────────────────────────────
#[test]
fn test_pick_first_returns_a() {
let a = String::from("abcde");
let result;
{
let b = String::from("xy");
// result borrows from `a` (lifetime 'a), so b can end here.
result = pick_first(&a, &b);
}
assert_eq!(result, "abcde");
}
// ── TextBuffer ────────────────────────────────────────────────────
#[test]
fn test_text_buffer_get_content() {
let buf = TextBuffer::new("Hello, World!");
assert_eq!(buf.get_content(), "Hello, World!");
}
#[test]
fn test_text_buffer_get_length() {
let buf = TextBuffer::new("Rust");
assert_eq!(buf.get_length(), 4);
}
#[test]
fn test_text_buffer_trim_to() {
let buf = TextBuffer::new("lifetime elision");
assert_eq!(buf.trim_to(8), "lifetime");
}
#[test]
fn test_text_buffer_trim_to_beyond_length() {
let buf = TextBuffer::new("short");
assert_eq!(buf.trim_to(100), "short");
}
// ── Excerpt ───────────────────────────────────────────────────────
#[test]
fn test_excerpt_content() {
let text = String::from("We choose to go to the Moon.");
let ex = Excerpt::new(&text);
assert_eq!(ex.content(), "We choose to go to the Moon.");
}
#[test]
fn test_excerpt_text_field() {
let s = "four score and seven years";
let ex = Excerpt::new(s);
assert_eq!(ex.text, s);
}
}
Deep Comparison
OCaml vs Rust: Lifetime Elision
Side-by-Side Code
OCaml
(* OCaml has no lifetime annotations — the GC handles memory safety *)
let first_word s =
match String.index_opt s ' ' with
| Some i -> String.sub s 0 i
| None -> s
type text_buffer = { content : string }
let get_content buf = buf.content
let get_length buf = String.length buf.content
Rust (idiomatic — elision applies)
// Compiler infers: fn first_word<'a>(s: &'a str) -> &'a str
fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or(s)
}
struct TextBuffer { content: String }
impl TextBuffer {
// Compiler infers: fn get_content<'a>(&'a self) -> &'a str
fn get_content(&self) -> &str { &self.content }
fn get_length(&self) -> usize { self.content.len() }
}
Rust (explicit — when elision cannot resolve ambiguity)
// Two input references: compiler cannot know which to tie the output to
fn pick_first<'a>(a: &'a str, _b: &str) -> &'a str { a }
// Struct holding a reference always requires explicit annotation
struct Excerpt<'a> { text: &'a str }
Type Signatures
| Concept | OCaml | Rust (elided) | Rust (explicit) |
|---|---|---|---|
| String slice | string | &str | &'a str |
| Function (1 ref in → ref out) | string -> string | fn f(s: &str) -> &str | fn f<'a>(s: &'a str) -> &'a str |
| Method returning borrowed field | t -> string | fn m(&self) -> &str | fn m<'a>(&'a self) -> &'a str |
| Struct with borrowed field | impossible (GC owns) | (must annotate) | struct S<'a> { f: &'a str } |
Key Insights
&self/&mut self propagates to outputs. When these rules yield one answer, you write nothing.struct Foo<'a> { field: &'a T } — there is no single obvious input to elide from.fn f(a: &str, b: &str) -> &str is a compile error because rule 2 no longer applies — two lifetimes, two possible sources. Rust forces you to pick: fn f<'a>(a: &'a str, b: &str) -> &'a str.fn get_content(&self) -> &str → fn get_content<'a>(&'a self) -> &'a str. This tells you the returned &str cannot outlive self.When to Use Each Style
Use elided lifetimes when: the function has a single input reference, or is a method where &self is the obvious donor — which covers the vast majority of real Rust code.
Use explicit lifetimes when: there are multiple input references and the output could borrow from more than one, when a struct stores a reference, or when you want to document a non-obvious lifetime relationship for readers.
Exercises
&str inputs and one &str output. Verify that an explicit lifetime annotation is required. Then add it.Parser<'a> { input: &'a str, pos: usize } and implement three methods that all return references — verify which ones need explicit annotations.fn split_at_first_space(s: &str) -> (&str, &str) returning two slices of the input and verify the elision rules apply.