On this page

Lifetime

Lifetimes ('a) are not runtime values—they are compile-time constraints on the valid scope of references. From elision rules (automatically inferred by the compiler) to explicit annotations (structs, variance, HRTBs), lifetime annotations essentially tell the borrow checker "which of these references outlives the others."

Lifetime is a Compile-Time Concept

Lifetime parameters ('a) are not runtime values—they are compile-time constraints: the valid scope of this reference cannot be longer than 'a. The compiler uses region inference in MIR to deduce the minimal lifetime.

Lifetime Elision

Most functions do not require explicit lifetime annotations—the compiler automatically infers them based on 3 rules:

  1. Each reference parameter without a lifetime annotation is assigned an independent lifetime (fn foo<'a, 'b>(x: &'a T, y: &'b U))
  2. If there is only one input lifetime, it is assigned to all output lifetimes (fn foo<'a>(x: &'a T) -> &'a U)
  3. If there are multiple input lifetimes and one of them is &self or &mut self, the self lifetime is assigned to all outputs

Rule 2 is the most common: a single reference parameter → the returned reference defaults to borrowing that parameter. Rule 3 is why method chaining works: the borrow of self is passed to the return value.

Lifetime in Structs

When a struct contains references, you must explicitly annotate the lifetime:

struct Excerpt<'a> {
    part: &'a str,                   // This reference cannot outlive 'a
}

impl<'a> Excerpt<'a> {
    fn announce(&self, msg: &str) -> &str {  // Rule 3: output borrows self
        println!("{}", msg);
        self.part
    }
}

The compiler's guarantee: an instance of Excerpt<'a> cannot outlive the referenced &'a str.

'static

'static is the only lifetime with a "special" meaning—it indicates that the referenced data lives for the entire duration of the program. Sources include:

  • String literals: the type of "hello" is &'static str—embedded in the binary at compile time, always valid
  • Box::leak(Box::new(T)) can produce &'static mut T
  • static variables and const references

Variance

Rust lifetimes have subtyping variance. Key rules to understand:

  • 'long: 'short (meaning 'long outlives 'short, so 'long satisfies the constraint of 'short)
  • &'a T: covariant in 'a (if 'long: 'short, then &'long T can be used where &'short T is expected)
  • &'a mut T: invariant in 'a (you cannot shorten the lifetime of a mutable reference—otherwise it might violate the aliasing XOR mutation rule)
fn assign<T>(to: &mut T, from: T) { *to = from; }

let mut x: &'static str = "hello";
{
    let y = String::from("world");
    // assign(&mut x, y.as_str());   // ERROR: T = &'static str (from to), but y is &'y str
}
// If this compiled, x would point to y's data → y gets dropped → dangling pointer

HRTB (Higher-Ranked Trait Bounds)

When a closure parameter has a lifetime but does not need to be explicitly declared:

fn apply<F>(f: F) where F: for<'a> Fn(&'a str) -> &'a str { ... }
// "f satisfies Fn(&'a str) -> &'a str for **any** lifetime 'a"
// No need to specify 'a at the call site—the compiler automatically quantifies it

PhantomData

When a struct has a lifetime but does not directly store a reference—use PhantomData to mark ownership/borrowing relationships:

use std::marker::PhantomData;

struct MyIter<'a, T> {
    ptr: *const T,                   // Raw pointer — does not carry Rust's borrowing relationship!
    _marker: PhantomData<&'a T>,     // Tells the compiler: "this struct logically holds &'a T"
}

References

  • Rust Book: Chapter 10.3 — Validating References with Lifetimes
  • Rust Reference: lifetime elision, variance
  • RFC: RFC 2094 (NLL), RFC 387 (Higher-Ranked Trait Bounds)

Keywords: lifetime, elision, 'static, variance, covariant, invariant, HRTB, PhantomData, region inference