| |

Rust 73 🦀 Advanced Lifetimes: Lifetime Subtyping and Bounds

⏱️ Estimated read: 18 min | |

Lifetimes are Rust’s mechanism for tracking how long references remain valid. The basic rules—every reference has a lifetime, the compiler infers them where possible, and explicit annotations are needed when the relationship between input and output lifetimes is ambiguous—are covered in any introduction to the language. The advanced material is about what happens when lifetimes interact with generics, trait objects, and variance. A lifetime bound ('a: 'b) says that 'a outlives 'b. Lifetime subtyping says that a reference with a longer lifetime can be used wherever a shorter one is expected. Variance says that a generic type’s lifetime parameter can be covariant, contravariant, or invariant, and that choice determines what coercions the compiler will accept .

These concepts are not academic. They are the reason &'static str can be passed to a function that takes &'a str, the reason Box<dyn Trait + 'a> is rejected when 'a is too short, and the reason some generic code requires T: 'a even though T is not a reference. The compiler enforces these rules to prevent dangling pointers, and when it rejects code that looks correct, the error message is almost always about one of these relationships.

Key point: A lifetime bound 'a: 'b means 'a outlives 'b. Lifetime subtyping means a longer lifetime can be used where a shorter one is expected, which is why &'static str coerces to &'a str. Variance determines whether a generic type preserves, reverses, or ignores the subtyping relationship of its lifetime parameter, and most types are covariant over their lifetimes while &mut T and Cell<T> are invariant.


Why lifetime bounds and subtyping exist

The dangling reference problem. A reference must never outlive the value it points to. The compiler enforces this by tracking lifetimes and rejecting any code where a reference could be used after the value is dropped. Lifetime bounds are how the programmer communicates the relationship between two lifetimes: 'a: 'b says that 'a is at least as long as 'b, so a reference with lifetime 'a can be used wherever a reference with lifetime 'b is expected . Without this relationship, the compiler cannot prove that the reference is valid at the point of use.

The generic container problem. A struct that holds a reference must declare the lifetime of that reference. struct Ref<'a, T>(&'a T) is valid. But what about struct Wrapper<T>(T)? Does T need to outlive some lifetime? The answer depends on whether T contains references. The T: 'a bound says that every reference inside T outlives 'a. It is necessary when the struct is used in a context where the value must live at least as long as 'a—for example, when it is stored in a Box<dyn Trait + 'a> . The bound is not about T itself being a reference; it is about any references inside T.

The trait object problem. A trait object Box<dyn Trait> has an implicit lifetime bound of 'static. That means the concrete type inside the box must not contain any non-'static references. If it does, the trait object must be written as Box<dyn Trait + 'a>, where 'a is the lifetime of the references inside the type . The bound is what tells the compiler how long the trait object is valid, and it is the reason that Box<dyn Trait> and Box<dyn Trait + 'static> are the same type.

The variance problem. If 'long: 'short, can a &'long T be used where a &'short T is expected? Yes, because a longer-lived reference is always valid for a shorter use. This is covariance. But can a &mut 'long T be used where a &mut 'short T is expected? No, because the shorter reference could be used to store a value that does not live long enough for the longer reference, and the longer reference would then have a dangling pointer. This is invariance. The variance of a type determines which coercions the compiler allows, and it is inferred from the type’s definition .


a. Lifetime bounds: 'a: 'b and T: 'a

A lifetime bound is written with a colon, just like a trait bound. 'a: 'b means that 'a outlives 'b. The compiler uses this to prove that a reference with lifetime 'a is valid for the entire duration of 'b .

// The 'a: 'b bound says that 'a outlives 'b
// It allows the function to return a reference with lifetime 'a
// when the input has lifetime 'b
fn longest<'a: 'b, 'b>(x: &'a str, y: &'b str) -> &'a str {
    // x is valid for 'a, which outlives 'b
    // the returned reference is valid for 'a
    if x.len() > y.len() { x } else { y }
}

fn main() {
    // 'static outlives any lifetime
    let s1 = "long string";
    let s2 = String::from("short");
    // s1 has 'static, s2 has a local lifetime
    // 'static: local is satisfied
    let result = longest(s1, &s2);
    println!("{}", result);
}

The 'a: 'b bound is not often written explicitly because the compiler can usually infer it from the function signature. But it is required in some generic contexts, particularly when a struct or trait definition needs to state the relationship between two lifetime parameters.

The T: 'a bound is different. It says that every reference inside T outlives 'a. It does not mean T is a reference; it means T is valid for at least as long as 'a. This is necessary when a value of type T is stored in a context that requires it to live for 'a, such as a trait object with a lifetime bound .

// A function that accepts a value and a reference
// The T: 'a bound says that T is valid for at least 'a
// This is necessary if T is stored in a structure that holds a reference with lifetime 'a
fn store<'a, T: 'a>(value: T, reference: &'a str) -> (&'a str, T) {
    // The returned tuple contains the reference and the value
    // T must be valid for 'a because the tuple is used together
    (reference, value)
}

fn main() {
    // T = i32, which has no references, so it trivially satisfies T: 'a
    let (r, v) = store(42, "hello");
    println!("{} {}", r, v);

    // T = &String, which contains a reference
    // The bound requires the reference inside T to outlive 'a
    let s = String::from("world");
    let (r, v) = store(&s, "hello");
    println!("{} {:?}", r, v);
}

The T: 'a bound is the source of the “does not live long enough” error in many generic contexts. When a struct has a lifetime parameter 'a and a type parameter T, the compiler often requires T: 'a because the struct holds a reference with lifetime 'a and a value of type T in the same struct. If T contained a reference shorter than 'a, the struct would have a dangling pointer .


b. Lifetime subtyping: longer to shorter

Lifetime subtyping is the rule that a reference with a longer lifetime can be used wherever a reference with a shorter lifetime is expected. The intuition is simple: if a reference is valid for a long time, it is also valid for a short time. The compiler uses this to coerce &'static str to &'a str when 'a is any shorter lifetime .

// A function that accepts a reference with lifetime 'a
fn print_str<'a>(s: &'a str) {
    println!("{}", s);
}

fn main() {
    // A string literal has 'static lifetime
    let s: &'static str = "hello";
    // 'static outlives any local lifetime, so it coerces to 'a
    print_str(s);

    // A String owned by main has a local lifetime
    let owned = String::from("world");
    // The reference to owned has the local lifetime
    print_str(&owned);
}

The coercion is automatic and happens at the call site. The compiler does not need an explicit cast. It knows that 'static outlives any lifetime, so &'static str is a subtype of &'a str for any 'a. This is the same relationship as a trait bound: 'static: 'a for every 'a .

The subtyping relationship is what makes the 'static lifetime special. It is the longest lifetime, and every other lifetime is a subtype of it. A reference with 'static can be used anywhere; a reference with a shorter lifetime cannot be used where 'static is required.


c. Variance: how generic types treat lifetime subtyping

Variance is the property of a generic type that determines whether it preserves, reverses, or ignores the subtyping relationship of its type parameters. The three kinds are:

Covariant. If 'long: 'short, then F<'long> is a subtype of F<'short>. The type preserves the subtyping relationship. Most types are covariant over their lifetimes: &'a T, Box<T>, Vec<T>, fn() -> T .

Contravariant. If 'long: 'short, then F<'short> is a subtype of F<'long>. The type reverses the relationship. Function arguments are contravariant: fn(&'a T) can be used where fn(&'long T) is expected, but not the other way around, because a function that accepts a shorter-lived reference is more permissive than one that requires a longer-lived reference.

Invariant. Neither F<'long> nor F<'short> is a subtype of the other. The type ignores the subtyping relationship. &'a mut T and Cell<T> are invariant over their lifetimes. The reason is that a mutable reference can write through the reference, and writing a shorter-lived value into a longer-lived reference would create a dangling pointer .

// Covariant: &'a T
// A &'long T can be used where &'short T is expected
fn covariant<'short>(x: &'short i32) {
    println!("{}", x);
}

fn main() {
    let value = 42;
    let long_ref: &'static i32 = &value;
    // &'static i32 coerces to &'short i32
    covariant(long_ref);
}

// Invariant: &'a mut T
// A &'long mut T cannot be used where &'short mut T is expected
fn invariant<'short>(x: &'short mut i32) {
    *x += 1;
}

fn main() {
    let mut value = 42;
    let long_ref: &'static mut i32 = &mut value;
    // invariant(long_ref); // error: lifetime mismatch
    // The compiler rejects this because the shorter reference
    // could be used to write a value that does not live long enough
}

The &'a mut T invariance is the reason some code that looks correct is rejected. If a function takes &'a mut T and returns &'a T, the compiler may reject the signature because the mutable reference is invariant. The fix is to use separate lifetime parameters for the input and the output, or to reborrow the reference .

The Box<dyn Trait + 'a> type is covariant over 'a. A Box<dyn Trait + 'static> can be used where Box<dyn Trait + 'a> is expected, because the 'static bound is stronger. The variance is inferred from the definition of Box and the trait object’s lifetime bound.


Complete Example Session

The following session builds a cache that stores references with lifetime bounds, demonstrates the T: 'a bound, and shows the variance difference between &T and &mut T.

// ============================================
// PART 1: A STRUCT WITH A LIFETIME BOUND
// ============================================
// The Cache holds a reference to a value with lifetime 'a
// The T: 'a bound is necessary because the cache stores both
// a reference with lifetime 'a and a value of type T
struct Cache<'a, T: 'a> {
    // The cached value
    value: T,
    // A reference to a name with lifetime 'a
    name: &'a str,
}

impl<'a, T: 'a> Cache<'a, T> {
    // Create a new cache
    fn new(value: T, name: &'a str) -> Self {
        Cache { value, name }
    }

    // Return the name
    fn name(&self) -> &'a str {
        self.name
    }
}

// ============================================
// PART 2: LIFETIME BOUNDS IN FUNCTIONS
// ============================================
// The 'a: 'b bound says that 'a outlives 'b
// The function returns a reference with lifetime 'a
// when the first argument has lifetime 'a
fn longest<'a: 'b, 'b>(x: &'a str, y: &'b str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

// ============================================
// PART 3: SUBTYPING WITH 'static
// ============================================
// A function that accepts any lifetime
fn print_str<'a>(s: &'a str) {
    println!("{}", s);
}

// ============================================
// PART 4: VARIANCE DEMONSTRATION
// ============================================
// Covariant: &T can be coerced to a shorter lifetime
fn covariant<'short>(x: &'short i32) -> i32 {
    *x
}

// Invariant: &mut T cannot be coerced to a shorter lifetime
fn invariant<'short>(x: &'short mut i32) {
    *x += 1;
}

fn main() {
    // ============================================
    // PART 5: USING THE CACHE
    // ============================================
    let name = String::from("cached");
    let cache = Cache::new(42, &name);
    println!("{}: {}", cache.name(), cache.value);

    // ============================================
    // PART 6: LIFETIME BOUND IN ACTION
    // ============================================
    let s1 = "long";
    let s2 = String::from("short");
    // s1 has 'static, s2 has a local lifetime
    // 'static: local is satisfied
    let result = longest(s1, &s2);
    println!("Longest: {}", result);

    // ============================================
    // PART 7: SUBTYPING
    // ============================================
    let static_str: &'static str = "static";
    // &'static str coerces to &'a str
    print_str(static_str);

    // ============================================
    // PART 8: VARIANCE
    // ============================================
    let value = 42;
    let long_ref: &'static i32 = &value;
    // Covariant: &'static i32 coerces to &'short i32
    let result = covariant(long_ref);
    println!("Covariant: {}", result);

    let mut mutable = 42;
    let long_mut: &'static mut i32 = &mut mutable;
    // Invariant: &'static mut i32 does not coerce to &'short mut i32
    // invariant(long_mut); // error: lifetime mismatch
    invariant(&mut mutable); // reborrow with a shorter lifetime
}

The eight parts cover the struct with a lifetime bound, the function with a lifetime bound, the subtyping example, the variance demonstration, and the main function that exercises each. What the session demonstrates is that the T: 'a bound is necessary when a struct holds both a reference and a value, that 'static coerces to any shorter lifetime because it is a subtype, and that &mut T is invariant while &T is covariant.


Quick Reference

Lifetime Bounds

SyntaxMeaning
'a: 'b'a outlives 'b
T: 'aEvery reference in T outlives 'a
'staticThe longest lifetime
dyn Trait + 'aTrait object with lifetime bound

Variance

TypeVarianceCoercion
&'a TCovariant&'long T → &'short T
&'a mut TInvariantNo coercion
Box<T>CovariantBox<&'long T> → Box<&'short T>
Cell<T>InvariantNo coercion
fn(T)Contravariantfn(&'short T) → fn(&'long T)
fn() -> TCovariantfn() -> &'long T → fn() -> &'short T

Subtyping

RelationshipExample
'static: 'aEvery lifetime is a subtype of 'static
&'long T <: &'short TCovariant reference
&'short mut T <: &'long mut TContravariant over the reference, invariant over T

T: 'a When Needed

SituationBound Required?
Struct holds both &'a and TYes
Trait object Box<dyn Trait + 'a>Yes
Function returns T with lifetime 'aDepends
T contains no referencesTrivially satisfied

Best Practices

✅ Do This:

// Add T: 'a when the struct holds both a reference and a value
struct Cache<'a, T: 'a> { value: T, name: &'a str }

// Use 'a: 'b when the output lifetime is the first input
fn longest<'a: 'b, 'b>(x: &'a str, y: &'b str) -> &'a str

// Use separate lifetimes for input and output on mutable references
fn split<'a, 'b>(x: &'a mut T, y: &'b mut T) -> (&'a T, &'b T)

// Use 'static for string literals
let s: &'static str = "hello";

❌ Don’t Do This:

// Forget T: 'a when the struct holds both
struct Cache<'a, T> { value: T, name: &'a str } // error: T may not live long enough

// Use a single lifetime for input and output on &mut
fn bad<'a>(x: &'a mut T) -> &'a T // may fail to compile

// Assume &mut T is covariant
let long: &'static mut i32 = ...;
let short: &'short mut i32 = long; // error: invariant

Common Pitfalls

PitfallWhy It HappensFix
“Does not live long enough”T: 'a bound missingAdd the bound
Cannot coerce &mut to shorterInvarianceReborrow with &mut *x
Box<dyn Trait> requires 'staticImplicit lifetime boundBox<dyn Trait + 'a>
Trait object with non-'static refMissing lifetime boundAdd + 'a
Function signature rejected&mut invarianceSeparate lifetimes

Real-World Examples

1. Struct with lifetime bound

struct Cache<'a, T: 'a> { value: T, name: &'a str }

2. Lifetime bound on function

fn longest<'a: 'b, 'b>(x: &'a str, y: &'b str) -> &'a str

3. 'static coercion

let s: &'static str = "hello";
fn takes<'a>(s: &'a str) {}
takes(s); // 'static coerces to 'a

4. Trait object with lifetime

let boxed: Box<dyn Trait + 'a> = Box::new(value);

5. Reborrow mutable reference

fn f<'a>(x: &'a mut i32) -> &'a i32 { &*x }

6. Separate lifetimes for split

fn split<'a, 'b>(x: &'a mut T, y: &'b mut T) -> (&'a T, &'b T)

7. Covariant Box

let long: Box<&'static str> = Box::new("hello");
let short: Box<&'a str> = long; // OK

8. Invariant Cell

let long: Cell<&'static str> = Cell::new("hello");
// let short: Cell<&'a str> = long; // error: invariant

9. T: 'a in trait bound

fn store<'a, T: 'a>(value: T, reference: &'a str) -> (&'a str, T)

10. Lifetime bound on associated type

trait Container<'a> { type Item: 'a; }

Visual

Lifetime Bound

┌──────────────────────────────────────────────┐
│  'a: 'b                                      │
│  ────────────                                │
│  'a ──────────────────────────────▶          │
│  'b ──────────────▶                          │
│                                              │
│  'a outlives 'b                              │
│  A reference with 'a is valid for all of 'b  │
└──────────────────────────────────────────────┘

Subtyping

┌──────────────────────────────────────────────┐
│  'static ─────────────────────────▶          │
│  'a      ──────────────▶                     │
│                                              │
│  &'static str can be used where &'a str is   │
│  because 'static outlives 'a                 │
│                                              │
│  &'a str cannot be used where &'static str   │
│  because 'a may not outlive 'static          │
└──────────────────────────────────────────────┘

Variance

┌──────────────────────────────────────────────┐
│  COVARIANT (&T, Box<T>)                      │
│  &'long T → &'short T  ✅                    │
│                                              │
│  INVARIANT (&mut T, Cell<T>)                 │
│  &'long mut T → &'short mut T  ❌            │
│                                              │
│  CONTRAVARIANT (fn(T))                       │
│  fn(&'short T) → fn(&'long T)  ✅            │
└──────────────────────────────────────────────┘

T: 'a Bound

┌──────────────────────────────────────────────┐
│  struct Cache<'a, T: 'a> {                   │
│    value: T,                                 │
│    name: &'a str,                            │
│  }                                           │
│                                              │
│  T must be valid for 'a                      │
│  because the cache stores both               │
│  a reference with 'a and a value of type T   │
└──────────────────────────────────────────────┘

Summary

ItemValue
Lifetime bound'a: 'b ('a outlives 'b)
Type lifetime boundT: 'a (every ref in T outlives 'a)
Subtyping&'long T usable where &'short T expected
'staticLongest lifetime, subtype of all
CovariantPreserves subtyping (&T, Box<T>)
ContravariantReverses subtyping (fn(T))
InvariantIgnores subtyping (&mut T, Cell<T>)
Trait objectBox<dyn Trait + 'a>
Implicit boundBox<dyn Trait> is Box<dyn Trait + 'static>

Key takeaways:

  • 'a: 'b means 'a outlives 'b. It allows a reference with lifetime 'a to be used where a reference with lifetime 'b is expected .
  • T: 'a means every reference in T outlives 'a. It is required when a struct holds both a reference with lifetime 'a and a value of type T, because the value must be valid for as long as the reference .
  • Subtyping allows longer lifetimes to be used where shorter ones are expected. &'static str coerces to &'a str because 'static outlives every other lifetime .
  • Variance determines which coercions are allowed. &T and Box<T> are covariant; &mut T and Cell<T> are invariant; fn(T) is contravariant. The variance is inferred from the type’s definition .
  • &mut T is invariant because it can write. If a &'long mut T could be coerced to &'short mut T, the shorter reference could store a value that does not live long enough, and the longer reference would then have a dangling pointer .
  • Trait objects have an implicit 'static lifetime bound. Box<dyn Trait> is Box<dyn Trait + 'static>. To allow non-'static references inside the trait object, add the + 'a bound explicitly .
  • The compiler’s error messages are about these relationships. “Does not live long enough” usually means a missing T: 'a bound. “Lifetime mismatch” on a &mut usually means the invariance prevented a coercion .

Remember: Lifetimes are the compiler’s proof that references are valid. The basic rules—every reference has a lifetime, explicit annotations are needed when the relationship is ambiguous—are the foundation. The advanced rules—bounds, subtyping, variance—are what allow the compiler to reason about lifetimes in generic code. A lifetime bound says one lifetime outlives another. Subtyping says a longer-lived reference can be used where a shorter-lived one is expected. Variance says whether a generic type preserves that subtyping relationship, reverses it, or ignores it. The &mut invariance is the one that surprises people most often, and the T: 'a bound is the one that causes the most “does not live long enough” errors. Understanding both is what turns lifetime errors from mysterious to mechanical.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!