Lifetimes, Region-Based Memory & Elision Rules

Master static lifetime regions, intersection constraints, lifetime syntax, compiler elision rules, and the 'static duration.

02. Memory Model, Ownership & Lifetimes7 min read

A reference in Rust is never permitted to outlive the resource to which it points. To enforce this invariant without a runtime garbage collector, the compiler uses Region-Based Memory Management parameterized by Lifetimes.

A lifetime is not a runtime timestamp or timer; it is a static region of the source code across which a reference must remain valid.

Runtime Lifetime CostStatic
0 Cycles

Lifetime annotations are fully evaluated and erased at compile time; they have zero runtime presence.

1. Formalizing Lifetime Regions#

Let L(v)L(v) be the program region where value vv is valid and initialized, and let L(r)L(r) be the region where reference rr (pointing to vv) exists. The fundamental Lifetime Validity Constraint dictates:

L(r)⊆L(v)L(r) \subseteq L(v)

If L(r)⊈L(v)L(r) \not\subseteq L(v), reference rr would outlive vv, creating a dangling pointer bug.

Architecture Diagram
Rendering diagram...

2. Lifetime Intersections in Functions#

When a function accepts multiple reference inputs and returns a reference, the compiler cannot deduce from the function signature alone which input buffer is being returned:

1
2
3
4
5
6
7
8
// The compiler requires explicit lifetime annotations here: fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }

The Mathematical Intersection Rule#

By declaring that both inputs and the output share lifetime 'a, we state that the output reference is bounded by the intersection of the lifetimes of xx and yy:

L(output)⊆L(x)∩L(y)L(\text{output}) \subseteq L(x) \cap L(y)

The caller cannot use the returned reference past the scope of whichever input argument lives for a shorter duration!

3. The Three Lifetime Elision Rules#

In early versions of Rust, every reference required explicit lifetime syntax. Today, the compiler applies three deterministic Elision Rules to infer lifetimes automatically:

1

Rule 1: Input Parameter Lifetimes

Each elided lifetime in the function parameter list is assigned a distinct, unique lifetime parameter:

fn parse(x: &str, y: &str) becomes fn parse<'a, 'b>(x: &'a str, y: &'b str).

2

Rule 2: Single Input Assignment

If there is exactly one input lifetime parameter (elided or explicit), that lifetime is assigned to all elided output lifetimes:

fn trim(s: &str) -> &str becomes fn trim<'a>(s: &'a str) -> &'a str.

3

Rule 3: The `&self` Method Rule

If there are multiple input lifetime parameters, but one of them is &self or &mut self, the lifetime of self is assigned to all elided output lifetimes.

If a signature has multiple inputs and does not match Rule 3, the compiler halts and requests explicit lifetime annotations.

4. The 'static Lifetime#

The 'static lifetime is the longest possible lifetime in Rust: it spans the entire execution duration of the program.

There are two distinct usages of 'static:

  1. Reference Duration (&'static T): The data itself lives for the entire program run (e.g. string literals stored in the binary's read-only Data Segment &'static str = "constant").
  2. Trait Bound (T: 'static): Asserts that type T contains no non-static references. Owned types like String, i32, or Vec<u8> satisfy T: 'static because they own their data completely.
T: 'static vs &'static T
A common beginner trap is assuming T: 'static means a variable must live forever. A String created locally satisfies T: 'static because it contains zero borrowed references, even though it will be dropped at the end of its local block!

5. Concept Verification#

Test your understanding of Rust lifetimes and elision rules:

Concept check

Do explicit lifetime annotations like 'a change when a variable is destroyed at runtime?

Concept check

Which lifetime elision rule automatically connects the output reference of a struct method to the struct's own lifetime?