Chapter 39: Lifetimes in Depth
Prerequisites
You will understand
- Variance: covariance, contravariance, invariance
- Higher-ranked trait bounds (
for<'a>) - Lifetime elision rules and when they fail
Reading time
Which Lifetime Substitutions Are Safe?
for<'a> Means “For Every Caller Lifetime”
Step 1 - The Problem
Beginner lifetime errors are usually about “this borrow does not live long enough.” Advanced lifetime reasoning is different. The hard problems are:
- how lifetimes compose in generic APIs
- when one lifetime can substitute for another
- why some positions are covariant and others invariant
- why trait objects default to
'staticin some contexts - why self-referential structures are fundamentally hard
Without this level of understanding, advanced library signatures look arbitrary and compiler errors feel mystical.
Step 2 - Rust’s Design Decision
Rust models lifetimes as relationships among borrows, not durations attached to values like timers. To make generic reasoning sound, it also tracks variance:
- where a longer lifetime may substitute for a shorter one
- where substitution is forbidden because mutation or aliasing would become unsound
Rust accepted:
- more abstract type signatures
- HRTBs and variance as advanced concepts
Rust refused:
- hand-waving lifetime substitution rules
- letting mutation accidentally launder one borrow lifetime into another
Step 3 - The Mental Model
Plain English rule: advanced lifetimes are about what relationships a type allows callers to substitute safely.
Variance answers: if I know T<'long>, may I use it where T<'short> is expected?
Step 4 - Minimal Code Example
#![allow(unused)]
fn main() {
fn apply<F>(f: F)
where
F: for<'a> Fn(&'a str) -> &'a str,
{
let a = String::from("hello");
let b = String::from("world");
assert_eq!(f(&a), "hello");
assert_eq!(f(&b), "world");
}
}
Step 5 - Line-by-Line Compiler Walkthrough
for<'a> means the closure or function works for any lifetime 'a, not one specific hidden lifetime.
So the compiler reads this as:
for all possible borrow lifetimes 'a, given &'a str, the function returns &'a str.
That is stronger than “there exists some lifetime for which this works.” It is universal quantification. This is why higher-ranked trait bounds show up in iterator adapters, callback APIs, and borrow-preserving abstractions.
The invariant is:
the callee must not smuggle in a borrow tied to one specific captured lifetime when the API promises it works for all caller-provided lifetimes.
Step 6 - Three-Level Explanation
Some functions must work with whatever borrow the caller gives them. for<'a> is how Rust says that explicitly.
Advanced lifetime tools matter in:
- parser and visitor APIs
- callback traits
- streaming or lending iterators
- trait objects carrying borrowed data
Variance matters because mutability changes what substitutions are safe. Shared references are usually covariant. Mutable references are invariant in the referenced type because mutation can break substitution assumptions.
Variance summary:
| Position | Usual variance intuition |
|---|---|
&'a T over 'a | covariant |
&'a T over T | covariant |
&'a mut T over T | invariant |
fn(T) -> U over input T | contravariant idea, though user-facing reasoning is often simplified |
| interior mutability wrappers | often invariant |
Why does this matter? Because if &mut T<'long> could be treated as &mut T<'short> too freely, code could write a shorter-lived borrow into a place expecting a longer-lived one. That would be unsound.
Lifetime Subtyping and Trait Objects
If 'long: 'short, then 'long outlives 'short. Shared references often allow covariance under that relationship.
Trait objects add another wrinkle. Box<dyn Trait> often means Box<dyn Trait + 'static> unless another lifetime is stated. That is not because trait objects are eternal. It is because the erased object has no borrowed-data lifetime bound supplied, so 'static becomes the default object lifetime bound in many contexts.
Self-Referential Structs
This is where many advanced lifetime ideas collide with reality.
A struct containing a pointer or reference into itself cannot be freely moved. That is why self-referential patterns usually require:
- pinning
- indices instead of internal references
- arenas
- or unsafe code with extremely careful invariants
The key lesson is not “lifetimes are annoying.” It is that moving values and borrowing into them are deeply connected.
Step 7 - Common Misconceptions
Wrong model 1: “for<'a> just means add another lifetime.”
Correction: it means universal quantification, which is much stronger than one named lifetime parameter.
Wrong model 2: “Variance is an academic topic with little practical value.”
Correction: it explains why many generic lifetime signatures compile or fail the way they do.
Wrong model 3: “Box<dyn Trait> means the object itself lives forever.”
Correction: it usually means the erased object does not contain non-static borrows.
Wrong model 4: “Self-referential structs are a lifetime syntax problem.”
Correction: they are fundamentally a movement and address-stability problem.
Step 8 - Real-World Pattern
You will see advanced lifetime reasoning in:
- borrow-preserving parser APIs
- callback traits that must work for any input borrow
- trait objects carrying explicit non-static lifetimes
- unsafe abstractions using
PhantomDatato describe borrowed relationships
Once you see lifetimes as substitution rules, not time durations, these APIs become much easier to read.
Step 9 - Practice Block
Code Exercise
Write a function bound with for<'a> Fn(&'a [u8]) -> &'a [u8] and explain why a closure returning a captured slice would not satisfy the bound.
Code Reading Drill
Explain what this means:
#![allow(unused)]
fn main() {
struct View<'a> {
bytes: &'a [u8],
}
}
Then explain how the story changes if the bytes come from inside the struct itself.
Spot the Bug
Why can this not work as written?
#![allow(unused)]
fn main() {
struct Bad<'a> {
text: String,
slice: &'a str,
}
}
Refactoring Drill
Take a self-referential design and redesign it using indices or offsets instead of internal references.
Compiler Error Interpretation
If the compiler says a borrowed value does not live long enough in a higher-ranked context, translate it as: “I promised this API works for any caller lifetime, but my implementation only works for one particular lifetime relationship.”
Step 10 - Contribution Connection
After this chapter, you can read:
- nontrivial parser and visitor signatures
- callback-heavy generic APIs
- trait objects with explicit lifetime bounds
- advanced unsafe code using
PhantomData<&'a T>
Good first PRs include:
- simplifying over-constrained lifetime signatures
- replacing accidental
'staticrequirements with precise lifetime bounds - improving docs on borrow relationships in public APIs
In Plain English
Advanced lifetimes are Rust’s way of saying exactly which borrowed relationships stay valid when generic code is reused in many contexts. That matters because serious library code cannot rely on “just trust me” borrowing; it has to describe precisely what substitutions are safe.
What Invariant Is Rust Protecting Here?
Borrow substitutions across generic code must preserve validity: a shorter-lived borrow must not be smuggled into a place that promises longer validity, especially through mutation or erased abstractions.
If You Remember Only 3 Things
for<'a>means “for every possible lifetime,” not “for one extra named lifetime.”- Variance explains which lifetime substitutions are safe and which are not.
- Self-referential structs are hard because movement and borrowing collide, not because lifetime syntax is missing.
Memory Hook
Lifetimes are not clocks. They are lane markings on a highway interchange telling you which vehicles may merge where without collision.
Flashcard Deck
| Question | Answer |
|---|---|
What does for<'a> mean? | The bound must hold for every possible lifetime 'a. |
| Why are mutable references often invariant? | Because mutation can otherwise smuggle incompatible lifetimes or types into a place that assumed a stricter relationship. |
What does 'long: 'short mean? | 'long outlives 'short. |
Why does Box<dyn Trait> often imply 'static? | Because object lifetime defaults often use 'static when no narrower borrow lifetime is specified. |
| Are lifetimes durations? | No. They are relationships among borrows and validity scopes. |
| Why are self-referential structs difficult? | Moving the struct can invalidate internal references into itself. |
| Where do HRTBs commonly appear? | Callback APIs, parser/visitor patterns, and borrow-preserving abstractions. |
| What does variance explain in practice? | Which lifetime or type substitutions are safe in generic positions. |
Chapter Cheat Sheet
| Need | Concept | Why |
|---|---|---|
| API works for any caller borrow | HRTB for<'a> | universal lifetime requirement |
| Understand substitution safety | variance | explains compile successes and failures |
| Non-static borrowed trait object | explicit object lifetime bound | avoid accidental 'static |
| Self-referential data | pinning, arenas, or indices | movement-safe design |
| Explain lifetime signature | relationship language | avoid duration-based confusion |