A lifetime names how long a borrow stays valid, written 'a. Lifetime
elision is the set of rules that lets you leave lifetimes out of a function
signature: the compiler fills them in from a fixed pattern. An elided
signature means exactly the same thing as the version with the lifetimes
written out.
Knowing these rules tells you when you do not need to write 'a. That
matters on this site because Rust submissions are checked with
clippy -D warnings. Clippy's needless_lifetimes lint is on by default and
reports an annotation that elision would have produced anyway, and
-D warnings turns that report into a failed check.
Some vocabulary first. Input lifetimes are the lifetimes that appear in a
function's parameter types. Output lifetimes are the ones in its return
type. A parameter carries a lifetime if its type contains at least one,
whether it is a reference like &str or a struct with a lifetime parameter
like View<'a>.
The rules, as rustc implements them
Rule 1. Each elided lifetime in the parameters becomes its own, distinct lifetime parameter.
fn f(a: &str, b: &str) // means fn f<'a, 'b>(a: &'a str, b: &'b str)
Two elided input lifetimes produce two separate names, and the two are unrelated. This rule applies to inputs only and never causes an error by itself.
Rule 2. An elided output lifetime is filled in only when exactly one parameter carries a lifetime, and every lifetime inside that one parameter is the same lifetime. That lifetime is then used for every elided position in the return type.
fn f(s: &str) -> &str // means fn f<'a>(s: &'a str) -> &'a str
fn f(s: &str, n: usize) -> &str // usize carries no lifetime: still elides
fn f(a: &str, b: &str) -> &str // two carriers: error[E0106]
E0106 is the "missing lifetime specifier" error. In the third line the
compiler cannot know whether the result borrows from a or from b, so it
asks you to say.
Rule 3. In a method whose receiver is a reference to Self (&self,
&mut self, self: &Self, and also self: Pin<&mut Self>), the receiver's
lifetime is used for every elided output lifetime, whatever the other
parameters carry.
impl Foo {
fn bar(&self, other: &str) -> &str // output borrows from self
}
Because of this rule, the body of bar may return something borrowed from
self but not other; returning other is a lifetime error.
Rule 3 is about the receiver, not about being inside an impl block. An
associated function with no self parameter gets rules 1 and 2 only. A
method that takes self by value has no receiver lifetime, so rule 2 decides
its output: fn bar(self, s: &str) -> &str borrows from s.
Where the usual wording is looser than rustc
The Rust Book states rule 2 as: "if there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters." The Rust Reference says: "If there is exactly one lifetime used in the parameters (elided or not), that lifetime is assigned to all elided output lifetimes." Read literally, both predict that this compiles:
fn f<'a>(x: &'a str, _y: &'a str) -> &str { x }
Only one lifetime, 'a, is used in the parameters; it appears twice. rustc
rejects it anyway:
error[E0106]: missing lifetime specifier
The same happens when the carriers are structs instead of references:
pub struct View<'a> { pub s: &'a str }
fn f<'a>(v: View<'a>, _w: View<'a>) -> &str { v.s } // error[E0106]
rustc counts lifetime-carrying parameters, not distinct lifetimes. Two
carriers means no output elision, even if both carry the same lifetime. The
fix is to name the output lifetime: -> &'a str.
The other half of rule 2 is about positions inside one parameter. This compiles:
pub struct View<'a> { pub s: &'a str }
fn f<'a>(x: &'a View<'a>) -> &str { x.s } // compiles
There are two lifetime positions (the &'a and the 'a in View<'a>), but
only one parameter, and both positions are the same lifetime, so the output
gets 'a. Elide both positions instead and it fails:
fn f(x: &View<'_>) -> &str { x.s } // error[E0106]
'_ is the explicit way to write an elided lifetime. By rule 1, the & and
the '_ here become two different lifetimes, so the parameter does not
supply a single lifetime and rule 2 does not apply.
So the precise statement is: several positions inside one parameter are fine as long as they are the same lifetime; a second lifetime-carrying parameter always blocks output elision (unless rule 3 applies).
Check your understanding For each signature, does elision fill in the output lifetime? Which part of rule 2 decides it? Show answer Hide answer
fn a(s: &str, n: usize) -> &str;
fn b(v: Vec<String>, s: &str) -> &str;
fn c(x: &str, y: &String) -> &str;
fn d(t: (&str, &str)) -> &str;
a: elides. usize carries no lifetime, so s is the only carrier, and it has one lifetime position. Rule 2 applies.
b: elides. Vec<String> owns its data and carries no lifetime either. This is the same shape as a.
c: error E0106. There are two lifetime-carrying parameters. It does not matter that both point at string data. (If y is used in the body, Clippy's ptr_arg lint also asks you to change &String to &str, but that does not affect elision.)
d: error E0106. There is only one parameter, but the tuple holds two elided references, which rule 1 turns into two different lifetimes. The "every position is the same lifetime" clause fails, exactly as it did for &View<'_>.
d is the case that needs both halves of the rule: counting parameters alone would predict that it elides.
A struct parameter: where the Book and the Reference differ
The Book's rule 1 only gives lifetimes to parameters that are references.
The Reference and rustc also count a parameter like View<'_>, which is not a
reference but carries a lifetime:
pub struct View<'a> { pub s: &'a str }
pub fn f(v: View<'_>, _n: usize) -> &str { v.s }
This compiles, and it passes Clippy with -D warnings on Rust 1.98.1. On the
Book's wording, View<'_> would get no input lifetime, there would be no
input lifetimes at all, and the function would be E0106. rustc treats v as
the single carrier and gives the output its lifetime.
If you try this yourself, two details decide whether it passes the
-D warnings check, and neither is about elision:
- In a library, the struct must be
publike the function. Apub fnthat takes a private type triggers theprivate_interfaceslint, which warns by default. - Write the parameter's lifetime as
'_, not as a named'a, and not hidden as plainView. Mixing styles between a parameter and the return type whose lifetimes are connected (for example a namedView<'a>with an elided-> &str, or a hiddenViewwith-> &str) triggers themismatched_lifetime_syntaxeslint, which warns by default on current stable Rust. The earlierfn f<'a>(x: &'a View<'a>) -> &strexample also triggers it: elision does supply the output lifetime there, but'ais named in the parameter and elided in the return type.
In other words, a signature can be correct as far as elision goes and still fail the check because of a separate style lint.
Two other places lifetimes are filled in
const and static items of reference type are 'static.
const GREETING: &str = "hi"; // means &'static str
static NAME: &str = "world"; // likewise
This is why writing &'static str there makes Clippy's
redundant_static_lifetimes lint suggest deleting the 'static.
Trait objects have default lifetime bounds. Box<dyn Error> means
Box<dyn Error + 'static>, and &'a dyn Error means
&'a (dyn Error + 'a). These defaults follow their own rules, separate from
the function rules above, and are covered in a separate article.
What elision does not do
Elision does not loosen any check. It only decides what you have to write.
Each elided signature has exactly one written-out form, and the compiler
checks callers and the body against that form. If you write the lifetimes out
yourself and they match what elision would produce, the meaning is identical;
in simple cases like the two function in the next question, Clippy's
needless_lifetimes lint asks you to remove them.
These rules do not apply to closures. This function compiles:
fn identity(x: &i32) -> &i32 { x }
but the closure with the same shape does not:
let identity = |x: &i32| x;
// error: lifetime may not live long enough
For a closure, the compiler does not apply rule 2 to connect the parameter's
lifetime to the return type. The annotated parameter &i32 accepts a
reference of any lifetime, while the return type is inferred as one single
lifetime, and that single lifetime cannot match every possible input. This is
a known limitation, covered in its own article. It is mentioned here so that
if you hit it, you know it is not a misunderstanding of the function rules.
Check your understanding
Under this site's clippy -D warnings check, which of these fails, and why is the failure not about the lifetimes being wrong?
Show answer
Hide answer
pub fn one(s: &str) -> &str { s.trim() }
pub fn two<'a>(s: &'a str) -> &'a str { s.trim() }
pub fn three<'a>(s: &'a str, t: &str) -> &'a str { let _ = t; s.trim() }
two fails.
All three are correct, and all three compile with plain rustc. two is rejected by clippy::needless_lifetimes, which warns by default; with -D warnings, the warning fails the check. Its annotation is exactly what elision would have produced, so writing it out adds nothing.
one is the elided form of two, so it passes. three needs its annotation: it has two lifetime-carrying parameters, so rule 2 cannot apply, and without 'a it is error E0106. needless_lifetimes stays silent here because removing 'a would not give the same signature.
A useful summary: the compiler reports an annotation that is missing, and Clippy reports one that is redundant.
Summary
- Every elided input lifetime becomes its own separate lifetime parameter.
- Elided output lifetimes are filled in only when one parameter carries lifetimes and all of its lifetime positions are the same lifetime.
- If the receiver is a reference to
Self(such as&selfor&mut self), its lifetime fills every elided output lifetime.
Also: const and static references are 'static; trait objects have
separate default bounds; an elided signature means the same as its
written-out form; and closures do not follow these rules.