Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

45 min
+ 25 min exercises
Variance

Which Lifetime Substitutions Are Safe?

covariant invariant &'long T usable as &'short T &mut T<'long> cannot safely become &mut T<'short> reader-only view permits narrower borrow mutation would let callers smuggle bad lifetimes in
HRTB

for<'a> Means “For Every Caller Lifetime”

for<'a> Fn(&'a str) -> &'a str 'short 'medium 'long must work must work must work not “one special lifetime” but every caller-provided borrow 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 'static in 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:

PositionUsual variance intuition
&'a T over 'acovariant
&'a T over Tcovariant
&'a mut T over Tinvariant
fn(T) -> U over input Tcontravariant idea, though user-facing reasoning is often simplified
interior mutability wrappersoften 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 PhantomData to 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 'static requirements 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

QuestionAnswer
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

NeedConceptWhy
API works for any caller borrowHRTB for<'a>universal lifetime requirement
Understand substitution safetyvarianceexplains compile successes and failures
Non-static borrowed trait objectexplicit object lifetime boundavoid accidental 'static
Self-referential datapinning, arenas, or indicesmovement-safe design
Explain lifetime signaturerelationship languageavoid duration-based confusion