Lifetime Elision and Advanced Lifetime Patterns
Master lifetime elision rules, input/output lifetime patterns, lifetime bounds, and variance
Lifetime Elision and Advanced Lifetime Patterns
Rust's lifetime elision rules make most lifetime annotations unnecessary. Understanding exactly when and how elision works is key to writing ergonomic Rust.
The Three Elision Rules
The compiler applies these rules automatically:
Rule 1 — Input Lifetimes
Each elided reference in function parameters gets a distinct lifetime:
// fn foo(x: &i32, y: &str)
// becomes:
fn foo<'a, 'b>(x: &'a i32, y: &'b str) {}Rule 2 — Single Input Lifetime
If there's exactly one input lifetime, it's assigned to all output references:
// fn first_word(s: &str) -> &str
// becomes:
fn first_word<'a>(s: &'a str) -> &'a str { unimplemented!() }Rule 3 — Method Receiver
If there's &self or &mut self, its lifetime is assigned to all output references:
impl Widget {
// fn get_name(&self) -> &str
// becomes:
fn get_name<'a>(&'a self) -> &'a str { unimplemented!() }
}Rule 2 and 3 only apply when there are no explicit output lifetimes. If you write a lifetime on the output, the rules don't apply.
Input vs Output Lifetimes
Input Lifetimes
Lifetimes on function parameters:
// 'a and 'b are input lifetimes
fn process<'a, 'b>(x: &'a str, y: &'b str) {}Output Lifetimes
Lifetimes on return values:
// 'a is an output lifetime (on the return type)
fn select<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}Lifetimes in Return Position Only
// Lifetime on return only — rare and usually wrong
// fn foo() -> &'static str { "static" } // OK — 'static works
// fn bar() -> &i32 { /* where does it point? */ } // ErrorElision in Method Signatures
struct Container<'a> {
data: &'a str,
}
impl<'a> Container<'a> {
// Elided: &self → output
fn get_data(&self) -> &str {
// Expanded: fn get_data<'b>(&'b self) -> &'b str
self.data
}
// Two input refs → no auto-output elision
// fn longer(&self, other: &str) -> &str {
// Expanded: fn longer<'b, 'c>(&'b self, other: &'c str) -> &'b str
fn longer<'b>(&'b self, other: &'b str) -> &'b str {
if self.data.len() > other.len() {
self.data
} else {
other
}
}
}Lifetime Elision and Generics
use std::fmt::Display;
// Elision works with generics
fn announce_and_return<'a>(announcement: &str, x: &'a str) -> &'a str {
println!("{announcement}");
x
}
// With trait bounds
fn longest_with_display<'a, T: Display + ?Sized>(
x: &'a T,
y: &'a T,
) -> &'a T
where
T: PartialOrd,
{
if x > y { x } else { y }
}Lifetime Bounds on Generic Types
// T must outlive 'a
struct Wrapper<'a, T: 'a> {
value: &'a T,
}
// T must be 'static (no non-static references)
fn process_static<T: 'static>(value: T) {
std::mem::drop(value);
}
// Higher-ranked trait bounds: for any lifetime
fn with_hrtb<F>(f: F)
where
F: for<'a> Fn(&'a str) -> &'a str,
{
println!("{}", f("hello"));
}Lifetime Subtyping (Variance)
Lifetimes can be in subtyping relationships:
// Covariant: &'a T is a subtype of &'b T if 'a: 'b
// Invariant: &'a mut T — must be exact
// Contravariant: fn(T) — reverse
struct Covariant<'a>(&'a str); // Covariant in 'a
struct Invariant<'a>(Cell<&'a str>); // Invariant in 'a
fn main() {
let long = String::from("long lived");
let short = String::from("short");
let cov: Covariant = Covariant(&long);
// Can assign to shorter lifetime:
let _: Covariant<'_> = cov; // OK: covariant
// Invariant would fail:
// let inv: Invariant = Invariant(Cell::new(&long));
// let _: Invariant<'_> = inv; // ERROR: invariant
}Lifetime Capture
// The impl Trait captures lifetimes from the function
fn make_debug<'a>(x: &'a str) -> impl std::fmt::Debug + 'a {
x
}
// Multiple lifetimes in impl Trait
fn make_cloneable<'a, 'b>(x: &'a str, y: &'b str) -> impl Clone + 'a + 'b {
(x, y)
}Common Elision Patterns
// Pattern 1: Reader
fn read(&self) -> &str { /* elided to &self lifetime */ }
// Pattern 2: Predicate
fn contains(&self, other: &str) -> bool { /* no output refs */ }
// Pattern 3: Factory
fn new(value: &str) -> Self { /* elided by rule 2 */ }
// Pattern 4: Multiple returns
fn parts(&self) -> (&str, &str) { /* all get &self lifetime */ }The Anonymous Lifetime
Use '_ to explicitly indicate elided lifetime:
struct Foo<'a> {
x: &'a str,
}
impl Foo<'_> { // '_ means the elided lifetime from impl
fn get(&self) -> &str { self.x }
}
// In function signatures
fn foo(x: &'_ str) -> &'_ str { x }Real-World: Lifetimes in a Cache
use std::collections::HashMap;
struct Index<'a> {
data: &'a str,
positions: HashMap<&'a str, usize>,
}
impl<'a> Index<'a> {
fn new(data: &'a str) -> Self {
let mut positions = HashMap::new();
for (i, line) in data.lines().enumerate() {
for word in line.split_whitespace() {
positions.entry(word).or_insert(i);
}
}
Index { data, positions }
}
fn find(&self, word: &str) -> Option<&'a str> {
// No explicit lifetime needed — elision works
self.positions.get(word).map(|&line| {
self.data.lines().nth(line).unwrap_or("")
})
}
}
fn main() {
let text = String::from("apple banana\ncherry date");
let index = Index::new(&text);
if let Some(line) = index.find("cherry") {
println!("Found: {line}"); // "cherry date"
}
}Practice Questions
- What are the three lifetime elision rules?
- When does Rule 2 NOT apply?
- What's the difference between input and output lifetimes?
- How are lifetimes elided in method signatures?
- What does
T: 'staticmean as a bound? - What's a higher-ranked trait bound (HRTB)?
- What's the difference between covariance and invariance for lifetimes?
- What does the
'_lifetime mean? - Why can't lifetimes be elided in
fn longest(x: &str, y: &str) -> &str? - How do you express "this function works for any lifetime" with a bounds?