| |

Rust 115 🦀 Subtyping and Lifetime Bound Subtyping Interactions

⏱️ Estimated read: 15 min | |

Rust has exactly one kind of subtyping: lifetime subtyping. A reference with a longer lifetime is a subtype of a reference with a shorter lifetime, and that single rule propagates through every generic type according to its variance. The interaction between subtyping and lifetime bounds—the 'a: 'b syntax and the implicit bounds that the compiler infers—is where the rules become subtle. A bound that looks like a declaration of intent is often a constraint on the type system that produces errors the programmer did not expect.

This chapter covers the interaction between subtyping and lifetime bounds. It begins with the definition of lifetime subtyping and the two ways it is expressed: the 'a: 'b bound and the implicit requirement of a function signature. It then explains the outlives rules that the compiler enforces—the requirement that every reference in a type outlives the type’s own lifetime parameter—and the closure and trait object cases where those rules produce the most errors. The final sections cover the where clauses that make the bounds explicit and the diagnostic patterns that distinguish a bound error from a variance error.


Key point: A lifetime bound is a subtyping constraint. 'a: 'b means 'a is a subtype of 'b, which means a value with lifetime 'a can be used where a value with lifetime 'b is expected. The compiler enforces this by requiring every component of a type to outlive the type’s own lifetime.


Why lifetime subtyping exists

The single subtyping relation. Rust’s type system has no class hierarchies and no inheritance. The only subtyping relation is between lifetimes: 'static is a subtype of every other lifetime, and a longer lifetime is a subtype of a shorter one. This is the relation that makes &'static str usable where &'a str is expected, and it is the relation that the variance rules propagate through generic types.

The outlives requirement. A type T with a lifetime parameter 'a must satisfy T: 'a, meaning every reference inside T must outlive 'a. The compiler enforces this by inferring outlives requirements from the type’s structure. A struct with a &'b u32 field requires 'b: 'a for the struct to satisfy T: 'a. The requirement is not written in the source; it is inferred and checked at every use site.

The bound syntax. The 'a: 'b syntax declares that 'a outlives 'b. It is used in generic parameter lists and where clauses to make the inferred requirement explicit. The bound is a constraint: the compiler must prove it at every call site, and the proof is the lifetime inference that the borrow checker performs.

The closure and trait object cases. The most common lifetime bound errors appear in closures and trait objects, because the compiler infers bounds that the programmer did not write. A closure that captures a reference has an inferred lifetime bound, and a Box<dyn Trait> has an inferred 'static bound unless the trait object is written with an explicit lifetime. These inferred bounds are the source of the “does not live long enough” errors that dominate the borrow checker’s output.


a. The definition and the two syntaxes

Lifetime subtyping is defined by the outlives relation. 'a: 'b means 'a outlives 'b, which means every point in 'b is also in 'a. The relation is reflexive and transitive, and 'static is a subtype of every lifetime.

// 'a outlives 'b: every value valid for 'b is valid for 'a.
fn example<'a, 'b>(x: &'a str, y: &'b str) -> &'b str
where
    'a: 'b,
{
    x  // the longer lifetime coerces to the shorter one
}

The same relation is expressed implicitly by the function signature. A function that takes &'a T and returns &'a T requires that the input and output share the lifetime, and the compiler infers the bound from the signature. The explicit where 'a: 'b is needed only when the relation is between two different lifetimes that the signature does not already relate.

// Implicit: the signature requires 'a to outlive the return.
fn first<'a>(x: &'a str, _y: &str) -> &'a str {
    x
}

// Explicit: the relation between 'a and 'b is declared.
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &'b str
where
    'a: 'b,
{
    if x.len() > y.len() { x } else { y }
}

The outlives rule for references is the base case: &'a T requires T: 'a, meaning the referent must outlive the reference. For a type with a lifetime parameter, the rule is that the type must outlive its own lifetime parameter: Vec<&'a T>: 'a requires every element to outlive 'a.


b. Outlives inference and the closure case

The compiler infers outlives requirements from the type’s fields. A struct with a &'b u32 field and a lifetime parameter 'a requires 'b: 'a for the struct to satisfy T: 'a. The rustc implementation uses the OutlivesEnvironment to collect these requirements and the OutlivesImpl to prove them at every use site.

Closures are the most common source of outlives errors because the compiler infers the closure’s lifetime bound from its captures. A closure that captures a reference has a bound like 'closure: 'a for the captured 'a, and the closure cannot be returned or stored in a type that requires a longer lifetime than the capture allows.

// The closure captures `x`, which has lifetime 'a.
// The closure cannot outlive 'a.
fn make_closure<'a>(x: &'a str) -> impl Fn() -> &'a str + 'a {
    move || x
}

// If the return type omitted the `+ 'a` bound, the compiler
// would infer `+ 'static` for the opaque type, and the
// function would fail to compile.

The + 'a in the return type is the explicit outlives bound. Without it, impl Fn() -> &'a str is interpreted as impl Fn() -> &'a str + 'static, which requires the closure to be 'static. The closure captures x, which is only 'a, so the requirement is unsatisfiable. The + 'a is the bound that tells the compiler the closure is only valid for 'a.

Trait objects have the same issue. Box<dyn Trait> has an implicit + 'static bound, which means the boxed value must own its data or capture only 'static references. Box<dyn Trait + 'a> is the form that allows the boxed value to borrow data with lifetime 'a.

// The box requires 'static: the boxed value cannot borrow.
fn boxed() -> Box<dyn Fn()> {
    Box::new(|| ())
}

// The box is valid for 'a: the boxed value can borrow.
fn boxed_borrowing<'a>(x: &'a str) -> Box<dyn Fn() -> &'a str + 'a> {
    Box::new(move || x)
}

c. Where clauses, HRTB, and the diagnostic patterns

The where clause is where lifetime bounds are declared explicitly. The for<'a> syntax, called a higher-ranked trait bound (HRTB), is the form that quantifies over all lifetimes rather than a specific one. It is used in function signatures that accept a function pointer or closure that must work for any lifetime.

// HRTB: the closure must accept any lifetime.
fn apply<F>(f: F)
where
    F: for<'a> Fn(&'a str) -> &'a str,
{
    let s = String::from("hello");
    f(&s);
}

The for<'a> is the bound that says the closure must be valid for every lifetime, not just one. A closure that returns a reference to a captured value is not valid for every lifetime, because the captured value has a specific lifetime. A closure that returns its argument is valid for every lifetime, because the argument’s lifetime is the one that the caller supplies.

The diagnostic patterns are the practical tool for reading these errors. A “does not live long enough” error with a note about a where clause is a bound error: the compiler could not prove the bound that the code requires. A “lifetime may not live long enough” error with a note about variance is a variance error: the type is invariant, and the coercion is rejected regardless of the bounds. The two errors look similar, but the fix is different.

ErrorNoteCauseFix
does not live long enoughwhere 'a: 'bBound not satisfiedAdd or strengthen the bound
lifetime may not live long enoughvarianceInvariant typeChange the field or the phantom
borrowed value does not live long enoughdrop orderValue dropped before its borrowReorder or clone
'static bound not satisfied+ 'staticOpaque type or trait objectAdd + 'a

Complete Example Session

// ============================================
// PART 1: THE BASE SUBTYPING
// ============================================
// A longer lifetime is a subtype of a shorter one.

fn shorten<'long, 'short>(x: &'long str) -> &'short str
where
    'long: 'short,
{
    x
}

// ============================================
// PART 2: THE IMPLICIT BOUND
// ============================================
// The signature relates the lifetimes without a where clause.

fn first<'a>(x: &'a str, _y: &str) -> &'a str {
    x
}

// ============================================
// PART 3: THE EXPLICIT BOUND
// ============================================
// The where clause declares the relation.

fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &'b str
where
    'a: 'b,
{
    if x.len() > y.len() { x } else { y }
}

// ============================================
// PART 4: THE OUTLIVES INFERENCE
// ============================================
// A struct with a reference field requires the field
// to outlive the struct's lifetime parameter.

struct Holder<'a> {
    value: &'a str,
}

// Holder<'a>: 'a requires the &'a str field to outlive 'a,
// which is trivially true because the field IS 'a.

// ============================================
// PART 5: THE CLOSURE OUTLIVES
// ============================================
// The closure captures x, and the return type must
// declare the bound.

fn make_closure<'a>(x: &'a str) -> impl Fn() -> &'a str + 'a {
    move || x
}

// Without + 'a, the opaque type is 'static, and
// the function does not compile.

// ============================================
// PART 6: THE TRAIT OBJECT OUTLIVES
// ============================================
// Box<dyn Trait> is 'static by default.

fn boxed_borrowing<'a>(x: &'a str) -> Box<dyn Fn() -> &'a str + 'a> {
    Box::new(move || x)
}

// ============================================
// PART 7: THE HRTB
// ============================================
// for<'a> quantifies over all lifetimes.

fn apply<F>(f: F)
where
    F: for<'a> Fn(&'a str) -> &'a str,
{
    let s = String::from("hello");
    f(&s);
}

// ============================================
// PART 8: THE DROP ORDER
// ============================================
// A value cannot borrow from a value that is dropped first.

// struct NoDrop<'a> {
//     value: &'a str,
//     _drop: DropGuard,  // dropped before value
// }

// The compiler rejects the type if DropGuard could
// access value, because value is dropped first.

// ============================================
// PART 9: THE DIAGNOSTIC
// ============================================
// The error message tells you which rule was violated.

// fn bad<'a>(x: &'a str) -> &'static str { x }
// ERROR: lifetime may not live long enough
// Note: the lifetime 'a must outlive 'static, which
// is only possible if 'a is 'static.

// ============================================
// PART 10: THE FULL PICTURE
// ============================================
// Subtyping is lifetime subtyping.
// Bounds are the explicit form of the relation.
// Outlives is the inferred requirement.
// Closures and trait objects have implicit bounds.

fn main() {
    let s = String::from("hello");
    let _ = shorten(&s);
    let _ = first(&s, "world");
    let _ = longest(&s, "world");
    let _ = make_closure(&s);
    let _ = boxed_borrowing(&s);
    apply(|x| x);
}

The ten parts build the subtyping and bound rules from the base case to the closure and trait object bounds. The pattern is consistent: the subtyping relation is a lifetime relation, the bounds are the explicit form, and the outlives requirements are the inferred form.


Quick Reference

Bound Syntax

FormMeaning
'a: 'b'a outlives 'b
T: 'aEvery reference in T outlives 'a
+ 'aOpaque type or trait object bounded by 'a
for<'a>HRTB, quantifies over all lifetimes

Implicit Bounds

TypeImplicit Bound
Box<dyn Trait>+ 'static
impl Trait+ 'static unless + 'a
Closure captureThe captured value’s lifetime
Reference fieldT: 'a for &'a T

Error Patterns

ErrorCauseFix
does not live long enoughBound not satisfiedAdd where bound
lifetime may not live long enoughVariance invariantChange field or phantom
borrowed value does not live long enoughDrop orderReorder fields or clone
'static bound not satisfiedMissing + 'aAdd + 'a to return type

Outlives Rules

RuleMeaning
&'a T: 'aThe referent outlives the reference
T: 'aAll references in T outlive 'a
'static: 'a'static outlives everything
Transitivity'a: 'b and 'b: 'c implies 'a: 'c

Best Practices

✅ Do This:

// Use explicit bounds when the relation is between lifetimes.
fn f<'a, 'b>(x: &'a str, y: &'b str) -> &'b str where 'a: 'b           // ✅
// Add + 'a to opaque types and trait objects that borrow.
fn f<'a>(x: &'a str) -> impl Fn() -> &'a str + 'a                      // ✅
// Use for<'a> for closures that accept any lifetime.
F: for<'a> Fn(&'a str) -> &'a str                                      // ✅

❌ Don’t Do This:

// Don't return a borrow without a bound.
fn f<'a>(x: &'a str) -> &'static str { x }  // ERROR                    // ❌
// Don't use Box<dyn Trait> for a borrowing value.
Box<dyn Fn() -> &'a str>  // implies + 'static                          // ❌
// Don't fight a variance error with a bound.
// The fix is the field, not the where clause.                          // ❌

Common Pitfalls

PitfallWhy It HappensFix
“does not live long enough”Bound not satisfiedAdd where 'a: 'b
Closure returned from function failsOpaque type is 'staticAdd + 'a
Box<dyn Fn() -> &'a str> rejectedTrait object is 'staticAdd + 'a
HRTB not satisfiedClosure returns captured referenceUse a closure that returns the argument
Drop order errorField dropped before its borrowReorder fields

Real-World Examples

1. Explicit Bound

fn f<'a, 'b>(x: &'a str) -> &'b str where 'a: 'b

2. Implicit Bound

fn f<'a>(x: &'a str) -> &'a str

3. Closure Bound

fn f<'a>(x: &'a str) -> impl Fn() -> &'a str + 'a

4. Trait Object Bound

Box<dyn Fn() -> &'a str + 'a>

5. HRTB

F: for<'a> Fn(&'a str) -> &'a str

6. Outlives

T: 'a

7. Static

'static: 'a

8. Transitivity

'a: 'b, 'b: 'c → 'a: 'c

9. Drop Order

struct S<'a> { a: &'a str, b: DropGuard }

10. Error Diagnosis

// lifetime may not live long enough → variance

Visual

The Subtyping Relation

┌─────────────────────────────────────────────────────────┐
│  'static                                                │
│    :                                                    │
│    ▼  (subtype)                                         │
│  'long                                                  │
│    :                                                    │
│    ▼                                                    │
│  'short                                                 │
│                                                         │
│  &'static T  <:  &'long T  <:  &'short T                │
│                                                         │
│  A longer lifetime can be used where a shorter is expected│
└─────────────────────────────────────────────────────────┘

The Outlives Requirement

┌─────────────────────────────────────────────────────────┐
│  struct Holder<'a> {                                    │
│      value: &'a str,                                    │
│  }                                                      │
│                                                         │
│  Holder<'a>: 'a                                         │
│  → every field must outlive 'a                          │
│  → &'a str: 'a is trivially true                        │
│                                                         │
│  The requirement is inferred, not written.              │
└─────────────────────────────────────────────────────────┘

The Closure Bound

┌─────────────────────────────────────────────────────────┐
│  WITHOUT + 'a:                                          │
│  impl Fn() -> &'a str                                   │
│  → interpreted as + 'static                             │
│  → closure must be 'static                              │
│  → ERROR if closure captures &'a str                    │
│                                                         │
│  WITH + 'a:                                             │
│  impl Fn() -> &'a str + 'a                              │
│  → closure is valid for 'a                              │
│  → compiles                                             │
└─────────────────────────────────────────────────────────┘

Error Diagnosis

┌─────────────────────────────────────────────────────────┐
│  "does not live long enough"                            │
│  → bound not satisfied                                  │
│  → fix: add where 'a: 'b                                │
│                                                         │
│  "lifetime may not live long enough"                    │
│  → variance invariant                                   │
│  → fix: change field or PhantomData                     │
│                                                         │
│  "borrowed value does not live long enough"             │
│  → drop order                                           │
│  → fix: reorder fields or clone                         │
│                                                         │
│  "'static bound not satisfied"                          │
│  → missing + 'a                                         │
│  → fix: add + 'a to the return type                     │
└─────────────────────────────────────────────────────────┘

Summary

ItemValue
SubtypingLifetime subtyping only
Bound syntax'a: 'b, T: 'a
OutlivesInferred requirement, T: 'a
Implicit 'staticBox<dyn Trait>, impl Trait
Explicit bound+ 'a on opaque types
HRTBfor<'a> quantifies over all lifetimes
Closure captureBound to the captured lifetime
Error diagnosisNote indicates bound vs variance vs drop order

Key takeaways:

  • Rust has exactly one subtyping relation. It is lifetime subtyping: a longer lifetime is a subtype of a shorter one. Every other subtyping relation is derived from this one by variance .
  • Bounds are the explicit form of the subtyping constraint. 'a: 'b declares that 'a outlives 'b. The compiler requires the bound to be satisfied at every use site, and the where clause is where it is declared .
  • Outlives requirements are inferred. A type T with a lifetime parameter 'a must satisfy T: 'a, meaning every reference inside T outlives 'a. The requirement comes from the fields, not from a written annotation.
  • Closures and trait objects have implicit 'static bounds. impl Fn() -> &'a str is + 'static unless written as + 'a. Box<dyn Trait> is + 'static unless written as Box<dyn Trait + 'a> .
  • The error message is the diagnostic. “does not live long enough” is a bound problem; “lifetime may not live long enough” is a variance problem; “borrowed value does not live long enough” is a drop-order problem. The fix depends on which one the note indicates.
  • HRTB is for functions that accept any lifetime. for<'a> Fn(&'a str) -> &'a str is the bound for a closure that returns its argument. It is the tool for the case where the lifetime is not known at the call site.

Remember: Subtyping and lifetime bounds are two views of the same relation. The subtype is the value with the longer lifetime; the bound is the constraint that the compiler must prove. The variance rules determine how the relation propagates through a generic type, and the outlives rules determine what the type requires of its components. When the compiler rejects a coercion, the error is one of these rules doing its job, and the fix is to satisfy the rule—not to fight the compiler.


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!