Skip to content
← All Rust articles

Rust explainer

The three elision rules, precisely

About 9 min to read. Allow extra time to work through examples.

How rustc fills in lifetimes you leave out of a function signature. The compiler's rule for output lifetimes is stricter than the wording in the Book or the Reference, and this article shows the signatures that tell them apart, checked on rustc 1.98.1.

This article uses Rust. You can make it your practice mode to browse related content across the site.

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
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 pub like the function. A pub fn that takes a private type triggers the private_interfaces lint, which warns by default.
  • Write the parameter's lifetime as '_, not as a named 'a, and not hidden as plain View. Mixing styles between a parameter and the return type whose lifetimes are connected (for example a named View<'a> with an elided -> &str, or a hidden View with -> &str) triggers the mismatched_lifetime_syntaxes lint, which warns by default on current stable Rust. The earlier fn f<'a>(x: &'a View<'a>) -> &str example also triggers it: elision does supply the output lifetime there, but 'a is 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
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

  1. Every elided input lifetime becomes its own separate lifetime parameter.
  2. Elided output lifetimes are filled in only when one parameter carries lifetimes and all of its lifetime positions are the same lifetime.
  3. If the receiver is a reference to Self (such as &self or &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.

step 5 of 22

Ownership III: Lifetimes, Explicitly

Up next Use lifetime elision for borrowed string results