Rust 73 🦀 Advanced Lifetimes: Lifetime Subtyping and Bounds
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
| Syntax | Meaning |
|---|---|
'a: 'b | 'a outlives 'b |
T: 'a | Every reference in T outlives 'a |
'static | The longest lifetime |
dyn Trait + 'a | Trait object with lifetime bound |
Variance
| Type | Variance | Coercion |
|---|---|---|
&'a T | Covariant | &'long T → &'short T |
&'a mut T | Invariant | No coercion |
Box<T> | Covariant | Box<&'long T> → Box<&'short T> |
Cell<T> | Invariant | No coercion |
fn(T) | Contravariant | fn(&'short T) → fn(&'long T) |
fn() -> T | Covariant | fn() -> &'long T → fn() -> &'short T |
Subtyping
| Relationship | Example |
|---|---|
'static: 'a | Every lifetime is a subtype of 'static |
&'long T <: &'short T | Covariant reference |
&'short mut T <: &'long mut T | Contravariant over the reference, invariant over T |
T: 'a When Needed
| Situation | Bound Required? |
|---|---|
Struct holds both &'a and T | Yes |
Trait object Box<dyn Trait + 'a> | Yes |
Function returns T with lifetime 'a | Depends |
T contains no references | Trivially 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| “Does not live long enough” | T: 'a bound missing | Add the bound |
Cannot coerce &mut to shorter | Invariance | Reborrow with &mut *x |
Box<dyn Trait> requires 'static | Implicit lifetime bound | Box<dyn Trait + 'a> |
Trait object with non-'static ref | Missing lifetime bound | Add + 'a |
| Function signature rejected | &mut invariance | Separate 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
| Item | Value |
|---|---|
| Lifetime bound | 'a: 'b ('a outlives 'b) |
| Type lifetime bound | T: 'a (every ref in T outlives 'a) |
| Subtyping | &'long T usable where &'short T expected |
'static | Longest lifetime, subtype of all |
| Covariant | Preserves subtyping (&T, Box<T>) |
| Contravariant | Reverses subtyping (fn(T)) |
| Invariant | Ignores subtyping (&mut T, Cell<T>) |
| Trait object | Box<dyn Trait + 'a> |
| Implicit bound | Box<dyn Trait> is Box<dyn Trait + 'static> |
Key takeaways:
'a: 'bmeans'aoutlives'b. It allows a reference with lifetime'ato be used where a reference with lifetime'bis expected .T: 'ameans every reference inToutlives'a. It is required when a struct holds both a reference with lifetime'aand a value of typeT, 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 strcoerces to&'a strbecause'staticoutlives every other lifetime . - Variance determines which coercions are allowed.
&TandBox<T>are covariant;&mut TandCell<T>are invariant;fn(T)is contravariant. The variance is inferred from the type’s definition . &mut Tis invariant because it can write. If a&'long mut Tcould 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
'staticlifetime bound.Box<dyn Trait>isBox<dyn Trait + 'static>. To allow non-'staticreferences inside the trait object, add the+ 'abound explicitly . - The compiler’s error messages are about these relationships. “Does not live long enough” usually means a missing
T: 'abound. “Lifetime mismatch” on a&mutusually 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.
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!