Lifetime Elision
Lifetime elision lets you omit explicit 'a annotations in common patterns. The compiler applies three rules to infer relationships between references in function signatures.
Recipe
fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
fn main() {
let line = String::from("hello world");
println!("{}", first_word(&line));
}When to reach for this: Writing clean APIs without redundant 'a when the elision rules cover your signature.
Working Example
struct Buffer<'a> { data: &'a [u8] }
impl<'a> Buffer<'a> {
fn get(&self, i: usize) -> Option<&u8> {
self.data.get(i)
}
fn slice(&self, range: std::ops::Range<usize>) -> Option<&[u8]> {
self.data.get(range)
}
}
fn main() {
let bytes = [1u8, 2, 3];
let buf = Buffer { data: &bytes };
println!("{:?}", buf.slice(0..2));
}What this demonstrates:
- Method elision ties returned refs to
&selflifetime - No explicit
'aongetorslicesignatures - Returned borrows cannot outlive
buf impl<'a>still required on struct with lifetime param
Deep Dive
The Three Elision Rules
- Each elided input reference gets a distinct lifetime parameter
- If exactly one input lifetime (or
&self), it is assigned to all outputs - If multiple input lifetimes but
&self, output borrows fromself
Examples
// Elided: fn foo(s: &str) -> &str
// Desugared: fn foo<'a>(s: &'a str) -> &'a str
// Elided method: fn bar(&self, x: &str) -> &str
// Output borrows from self, not x (rule 3)When Elision Fails
- Multiple input refs and return ref without
&self- must annotate - Struct definitions with references - usually explicit lifetimes
- Complex higher-ranked trait bounds
Gotchas
- Assuming return borrows from any parameter - Only elision rules decide. Fix: Annotate when multiple inputs compete.
- Method returning ref from argument not self - Needs explicit lifetimes. Fix:
fn merge<'a>(&self, other: &'a str) -> &'a stretc. - Confusing elision with inference inside body - Elision is signature-only. Fix: Body inference is separate.
- Changing
&selfto&mut selfsubtly - Elision still applies but borrow rules differ. Fix: Re-check conflicts. - Trait methods without elision guarantee across editions - Read trait docs. Fix: Explicit lifetimes on public traits for clarity.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
Explicit 'a everywhere | Public API clarity | Simple methods where elision works |
Return owned String | Avoid lifetime coupling | Hot path needs borrow |
| Associated type output | Complex trait APIs | Simple fn suffices |
| GATs (advanced) | Iterator-like borrows | Stable simple signatures |
FAQs
Do elision rules apply to closures?
Closures infer lifetimes differently; elision rules are for fn/impl items.
Can I elide struct fields?
No. Struct fields with references need explicit lifetime parameters.
What about `fn(&str, &str) -> &str`?
Fails elision - multiple inputs, ambiguous output. Annotate: <'a>.
Does `-> &str` in method always mean self?
With &self and one other ref, output is from self per rule 3.
Are async fn signatures elided?
Async desugaring adds hidden lifetime on return Future; prefer 'static captures when possible.
Edition 2024 changes?
Core elision rules stable; always verify with compiler on new signatures.
Can elision confuse readers?
Sometimes. Public traits may document explicit lifetimes for readability.
Elision and `impl Trait` returns?
Opaque return types have separate rules; borrows inside must still be valid.
How to see desugared signature?
rustc -Zunpretty=expanded or trust rust-analyzer hover on errors.
When must I learn explicit annotations?
Whenever compiler rejects elided signature - next page is annotations.
Related
- Lifetime Annotations - explicit
'a - Lifetimes Explained - concepts
- Non-Lexical Lifetimes & Reborrows - loan end points
- The Borrow Checker - enforcement
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.