Rust 115 🦀 Subtyping and Lifetime Bound Subtyping Interactions
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.
| Error | Note | Cause | Fix |
|---|---|---|---|
| does not live long enough | where 'a: 'b | Bound not satisfied | Add or strengthen the bound |
| lifetime may not live long enough | variance | Invariant type | Change the field or the phantom |
| borrowed value does not live long enough | drop order | Value dropped before its borrow | Reorder or clone |
'static bound not satisfied | + 'static | Opaque type or trait object | Add + '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
| Form | Meaning |
|---|---|
'a: 'b | 'a outlives 'b |
T: 'a | Every reference in T outlives 'a |
+ 'a | Opaque type or trait object bounded by 'a |
for<'a> | HRTB, quantifies over all lifetimes |
Implicit Bounds
| Type | Implicit Bound |
|---|---|
Box<dyn Trait> | + 'static |
impl Trait | + 'static unless + 'a |
| Closure capture | The captured value’s lifetime |
| Reference field | T: 'a for &'a T |
Error Patterns
| Error | Cause | Fix |
|---|---|---|
| does not live long enough | Bound not satisfied | Add where bound |
| lifetime may not live long enough | Variance invariant | Change field or phantom |
| borrowed value does not live long enough | Drop order | Reorder fields or clone |
'static bound not satisfied | Missing + 'a | Add + 'a to return type |
Outlives Rules
| Rule | Meaning |
|---|---|
&'a T: 'a | The referent outlives the reference |
T: 'a | All 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| “does not live long enough” | Bound not satisfied | Add where 'a: 'b |
| Closure returned from function fails | Opaque type is 'static | Add + 'a |
Box<dyn Fn() -> &'a str> rejected | Trait object is 'static | Add + 'a |
| HRTB not satisfied | Closure returns captured reference | Use a closure that returns the argument |
| Drop order error | Field dropped before its borrow | Reorder 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
| Item | Value |
|---|---|
| Subtyping | Lifetime subtyping only |
| Bound syntax | 'a: 'b, T: 'a |
| Outlives | Inferred requirement, T: 'a |
Implicit 'static | Box<dyn Trait>, impl Trait |
| Explicit bound | + 'a on opaque types |
| HRTB | for<'a> quantifies over all lifetimes |
| Closure capture | Bound to the captured lifetime |
| Error diagnosis | Note 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: 'bdeclares that'aoutlives'b. The compiler requires the bound to be satisfied at every use site, and thewhereclause is where it is declared . - Outlives requirements are inferred. A type
Twith a lifetime parameter'amust satisfyT: 'a, meaning every reference insideToutlives'a. The requirement comes from the fields, not from a written annotation. - Closures and trait objects have implicit
'staticbounds.impl Fn() -> &'a stris+ 'staticunless written as+ 'a.Box<dyn Trait>is+ 'staticunless written asBox<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 stris 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.
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!