Lifetimes, Region-Based Memory & Elision Rules
Master static lifetime regions, intersection constraints, lifetime syntax, compiler elision rules, and the 'static duration.
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.
Lifetime annotations are fully evaluated and erased at compile time; they have zero runtime presence.
1. Formalizing Lifetime Regions#
Let be the program region where value is valid and initialized, and let be the region where reference (pointing to ) exists. The fundamental Lifetime Validity Constraint dictates:
If , reference would outlive , creating a dangling pointer bug.
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:
12345678// 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 and :
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:
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).
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.
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:
- 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"). - Trait Bound (
T: 'static): Asserts that typeTcontains no non-static references. Owned types likeString,i32, orVec<u8>satisfyT: 'staticbecause they own their data completely.
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:
Do explicit lifetime annotations like 'a change when a variable is destroyed at runtime?
Which lifetime elision rule automatically connects the output reference of a struct method to the struct's own lifetime?