Skip to content
advanced Phase 4 · Lifetimes and Bounds

Lifetime Bounds and Outlives Relations

Constrain multiple lifetimes and generics with outlives relationships.

45m
0 problems
Topic Progress 0%

Reading `'b: 'a`: the Colon Says Who Outlives Whom

A single lifetime parameter constrains nothing about other lifetimes — it just names one span. The moment your signature holds two spans that are related but not identical, you need the outlives bound, and its direction is where most mistakes happen. Elision cannot help: it assigns lifetimes one signature at a time and never relates two of them, so a stated bound is the only way to express the connection.

fn clip<'inner, 'outer: 'inner>(data: &'inner str, _context: &'outer str) -> &'inner str {
    data
}

Read the bound 'outer: 'inner aloud: "'outer outlives 'inner" — the context span is at least as long as the data span, so the result, tied to 'inner, stays valid for the whole context. The colon always points from the longer-lived lifetime toward the shorter-lived one, exactly like T: Trait reads "T satisfies Trait". A useful visual: the region named 'inner sits inside the region named 'outer, and every reference valid for 'outer is automatically valid for 'inner — but never the other way around.

Where does a bound like this earn its keep? Whenever a function or type must relate two spans rather than unify them. The classic shape is a helper that returns one of several borrowed fields with different owners:

struct Window<'a, 'b> {
    head: &'a [u8],
    tail: &'b [u8],
}

fn prefer_head<'a, 'b: 'a>(w: Window<'a, 'b>) -> &'a [u8] {
    w.head
}

The bound documents the invariant the body relies on: the tail outlives the head, so returning a head-slice as &'a is safe for as long as any caller might reasonably hold it. Without the bound, &'a [u8] would say nothing about how the two spans relate, and a caller that kept the result past the head's end would be type-correct but memory-unsound — so the compiler demands the relation before it accepts the signature at all.

Bounds also combine with each other. <'a, 'b: 'a, 'c: 'b> is a chain: 'c outlives 'b, which outlives 'a. And the most-used special case is 'a: 'static — "'a outlives the whole program", which forces 'a to be 'static, a trick you will see in trait definitions that refuse borrowed data. Direction errors are loud. If you write 'a: 'b where the body actually returns 'b data typed as 'a, the mismatch surfaces the first time a caller passes a short 'b span: error: lifetime 'a may not live long enough, or the body-level variant when the compiler checks the function itself. There is no run-time mode in which the wrong direction slips through — the obligation is discharged before the program exists.

Bounds also appear on impl blocks, where they relate the receiver's span to an argument's:

struct Snapshot<'a> { view: &'a [u8] }

impl<'a> Snapshot<'a> {
    fn with_suffix<'b: 'a>(&self, suffix: &'b [u8]) -> &'a [u8] {
        self.view   // valid: every use site may rely on the 'a span
    }
}

And one bound has a fixed meaning worth memorizing: where 'a: 'static can only be satisfied by 'a being 'static, so libraries use it to declare that a generic never accepts borrowed data — the lifetime-flavored way of saying "owned values only".

Where Clauses: Lifetime Bounds Next to Trait Bounds

Inline bounds (<'a, 'b: 'a>) are fine for one relation, but signatures routinely need lifetime bounds and trait bounds at once. That is what where clauses are for: they move every constraint after the arrow, in one readable block, without changing meaning.

fn diff<'a, 'b, T>(old: &'a T, new: &'b T) -> &'a str
where
    'b: 'a,
    T: Display + 'a,
{
    let _ = new;
    old.label()
}

Three things are worth unpacking. First, every parameter in a where clause must already be declared in the angle brackets — where clauses constrain, they never introduce. Second, T: Display + 'a mixes the two bound kinds on one line: the type must implement the trait and embed no borrows shorter than 'a. Third, the lifetime bound 'b: 'a sits here exactly as it would inline; the two spellings are interchangeable, and the choice is purely about legibility. As a rule of thumb: inline bounds for one or two constraints next to the parameter they describe, where clauses once a signature has a type bound, a lifetime bound, and a trait bound competing for attention.

The T: 'a bound deserves its own moment, because it is the one that shows up in real library code. A generic T might itself contain references:

struct Caption<'a, T> {
    text: &'a str,
    payload: T,
}

fn preview<'a, T: 'a>(c: &Caption<'a, T>) -> &'a str {
    c.text
}

T: 'a says "whatever T is, it contains no borrows shorter than 'a". Why needed? Because T could be a String (fine for any 'a) or a &'short str (not fine unless 'short: 'a). The compiler cannot see inside an uninstantiated T, so you assert the relation you need and callers discharge it at instantiation. Struct definitions take the same bound: struct Caption<'a, T: 'a> is the conservative default, and omitting it only works when nothing in the struct's methods requires the relation.

One asymmetry to remember when reading errors: lifetime bounds in where clauses must name parameters declared before or in the same list — where 'a: 'b, 'b: 'a is accepted (it simply forces the two spans to be equal), while naming an undeclared 'c is a straight parse error. Bounds are a constraint language, not a declaration site.

The same block works on impl headers, which is where most real where clauses live:

use std::fmt::{self, Display};

struct Wrapper<'a, T> { label: &'a str, value: T }

impl<'a, T> Display for Wrapper<'a, T>
where
    T: Display,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}: {}", self.label, self.value)
    }
}

Note what is absent: no T: 'a bound. The methods only read self.value during the call, so nothing requires T to outlive 'a; the borrowed label's span and the payload's span never need to relate. Adding + 'a here would compile but advertise a constraint the code does not use — and unused bounds are not free, they push the obligation onto every caller. That discipline applies to lifetime bounds exactly as it does to trait bounds: state the relation your reasoning depends on, and nothing more. The genuine + 'a case in a where clause is the one you saw above — generic data that must remain valid alongside a borrow — and it shows up most often in APIs storing payloads next to borrowed keys: fn cache<E>(key: &'a str, err: E) where E: Error + 'a promises the error value stays valid for the key's span, which is what a registry holding both would demand.

Implied Bounds, Variance, and Why Bad Assumptions Never Ship

The promise of outlives bounds is simple: if an assumption you wrote is wrong, the program does not compile. A function that claims 'b: 'a is checked at every call site against the actual spans of the arguments; a body that returns something shorter than the signature promises is checked at the return. There is no configuration, no feature flag, no run-time mode in which the assumption silently fails — the same guarantee you rely on for ownership and borrowing, applied between lifetimes.

What saves you from writing bounds you do not need is the compiler's set of implied bounds. When a function takes &'a Window<'x, 'y>, the compiler already knows the struct's fields are valid for 'x and 'y — it does not make you restate those relations on every signature, because the struct definition asserted them once. This is why real code rarely shows explicit outlives bounds on borrowed structs, and why the bounds you do see are usually on generic parameters (T: 'a), which are never implied.

The other concept that keeps bounds honest is variance — how a type's lifetime parameters behave under subtyping. &'long T coerces to &'short T, so & is covariant in its lifetime: a longer-lived reference is a subtype of a shorter-lived one, and the compiler shortens freely. &'long mut T is different — it is invariant — because if it were covariant, you could keep the shorter-lived mutable reference and overwrite the memory after the longer-lived one was done reading. Invariance is the mechanism behind errors that look puzzling at first:

fn store<'a>(slot: &mut &'a str, value: &'a str) { *slot = value; }

let mut r: &str;
{
    let owned = String::from("inner");
    r = "outer";                // 'static coerces down to any 'a
    store(&mut r, &owned);       // forces r: &'a str where 'a borrows `owned`
}   // error: `owned` does not live long enough
println!("{r}");                // ...because r must still be valid here

Because &mut is invariant, store forces r's borrow to be exactly 'a, tied to owned; no shortening saves it. Knowing that & shortens and &mut does not explains most "the lifetime bound is right but it still fails" bugs.

Practical guidance to close: in application code, explicit outlives bounds are rare — prefer letting elision and implied bounds do the work, and add a bound only when the compiler demands a relation it cannot infer. In library code, bounds become part of the contract you document: 'b: 'a tells callers their shorter-lived data may be passed alongside longer-lived data, and T: 'static tells them borrowed payloads are refused. Interviewers probe exactly this distinction — "when would you write a where clause with a lifetime bound?" — so the honest answer is: when two spans must be related for the signature's promise to be sound, and nowhere else.

A compact recap of the bounds production code actually writes:

Bound Written on What it means
'b: 'a fn/impl generics caller supplies spans where 'b is at least as long as 'a
T: 'a generic type parameters the payload embeds no borrows shorter than 'a
'a: 'static trait definitions owned values only; borrowed payloads are rejected
dyn Trait + 'a trait objects the object may borrow, overriding the 'static default

Everything else — elision inside signatures, implied bounds from borrowed fields, coercing 'static down to a shorter span — the compiler handles without being asked.

Quiz

1. What does the bound `'b: 'a` assert?

Question 1 options

2. Why does `fn preview<'a, T: 'a>(c: &Caption<'a, T>) -> &'a str` need the bound `T: 'a`?

Question 2 options

3. Which of these is a legal and meaningful use of a where clause?

Question 3 options

4. A function with a correct-looking outlives bound still fails with `lifetime may not live long enough`. What does that prove?

Question 4 options

Flashcards

Question

How do you read `'b: 'a` — which one is longer-lived?

Answer

`'b: 'a` means `'b` outlives `'a`: `'b` is at least as long, so the `'a` region nests inside `'b`. The colon points from longer to shorter, mirroring `T: Trait` (left satisfies right).

Question

What does `T: 'a` mean, and why can't the compiler infer it?

Answer

It asserts that `T` contains no borrows shorter than `'a` (T outlives 'a). `T` is an opaque generic — it could instantiate to `&'short str` — so the compiler cannot verify the relation without the bound; callers discharge it at instantiation.

Question

When do you write bounds in a where clause instead of inline?

Answer

When a signature combines trait bounds and lifetime bounds — where clauses keep `T: Display + 'a` and `'b: 'a` readable after the arrow. Both forms are equivalent; where clauses constrain already-declared parameters and can never introduce new ones.

Question

What are implied bounds, and which parameters never get them?

Answer

Borrowed struct fields create relations the compiler assumes automatically — a `&'a Window<'x, 'y>` already guarantees the field spans without restating bounds per signature. Generic type parameters never get implied bounds: `T: 'a` must always be written and checked.

Question

Why is `&'a T` covariant in `'a` but `&'a mut T` invariant?

Answer

`&'long T` can safely coerce to `&'short T` (a read-only view of longer data is a valid shorter view). `&mut` must not shorten: otherwise a longer-lived mutable reference could outlive the shorter one and access memory after it ended. Invariance is why many `&mut`-taking helpers need exact lifetime matches.

Revision Notes

Key Takeaways

  • 1. `'b: 'a` reads "`'b` outlives `'a`" — the longer-lived span sits on the left, mirroring `T: Trait`
  • 2. Where clauses hold lifetime and trait bounds together but only constrain parameters already declared in angle brackets
  • 3. `T: 'a` is the assertion that a generic embeds no borrows shorter than `'a`, and it is never inferred for opaque `T`
  • 4. Implied bounds from borrowed struct fields spare signatures from restating field relations; generic parameters always need explicit bounds
  • 5. `&` is covariant and shortens freely; `&mut` is invariant — the source of most "correct bound, still fails" errors

Interview Tips

  • • "What does `'b: 'a` mean?" — answer with direction first ("'b outlives 'a"), then the nesting picture; many candidates reverse it under pressure
  • • "When would you write a where clause?" — when trait bounds and lifetime bounds compete for space; state that both forms are equivalent and where clauses cannot introduce parameters
  • • "Why is `T: 'a` needed in that struct/impl?" — because `T` could instantiate to a short borrow; the compiler cannot see inside an uninstantiated generic
  • • "Explain covariance of `&'a T`" — long-to-short coercion is safe for reads; contrast with `&'a mut T` invariance and give the overwrite-after-read hazard as the reason

Cheat Sheet

Lifetime Bounds and Outlives Relations

Reading the colon: 'b: 'a → "'b outlives 'a" — 'b ≥ 'a, 'a nests inside 'b. Chains: <'a, 'b: 'a, 'c: 'b>.

Where to put them:

Form Use when
<'a, 'b: 'a> inline one or two relations next to the parameter
where 'b: 'a, T: Display + 'a multiple trait + lifetime bounds on one signature

T: 'a reads: "T embeds no borrows shorter than 'a" — required because generic T could be &'short str; never inferred for uninstantiated parameters.

Implied vs stated: borrowed struct fields imply their field relations (&'a Window<'x, 'y> needs no restatement); generic parameters never imply bounds.

Variance quick reference:

&'a T        covariant in 'a   (long → short coercion is free)
&'a mut T    invariant in 'a   (exact match required)

Failure model: wrong outlives assumptions fail at compile time — at the call site (arguments too short) or in the body (returned data shorter than promised). Never at run time.

Application vs library: app code rarely needs explicit lifetime bounds; library contracts state them deliberately ('b: 'a for related spans, T: 'static to refuse borrowed payloads).

Continue Learning

Related Reading