A variable's lifetime denotes the span during which the data it refers to remains valid. This span is determined statically by the compiler. Several widespread misconceptions can lead to confusion when reasoning about lifetimes.
1) Generics Include More Than Owned Types
It’s tempting to think of a generic type Item as representing only owned types like i32 or String. In reality, Item encompasses any type, including references and nested references: &i32, &&i32, &mut &i32, etc. Thus:
Itemis a supertype of both&Itemand&mut Item.&Itemand&mut Itemare disjoint sets.
Attempting overlapping trait implementations shows this:
#![allow(dead_code)]
trait Marker {}
impl<Item> Marker for Item {} // covers everything
// impl<Item> Marker for &Item {} // error: conflict with above
// impl<Item> Marker for &mut Item {} // error: conflict with above
// These are fine because they no longer overlap
impl<Item> Marker for &Item {}
impl<Item> Marker for &mut Item {}
Key takeaways:
Itemincludes references.&Itemand&mut Itemcannot share implementations withItemitself.
2) Item: 'static Does Not Mean "Lives Forever"
Many assume val: 'static implies the data lives until program termination and is immutable. This confuses the bound (Item: 'static) with the concrete type &'static Item.
&'static Itemis a reference guaranteed valid for the entire run.Item: 'staticmeans the item can be held indefinitely — it may be an owned type (String,Vec<u8>) or a'staticreference.
Owned types satisfy 'static regardless of their actual lifespan:
use rand;
fn discard<Item: 'static>(item: Item) {
std::mem::drop(item);
}
fn main() {
let mut data = Vec::new();
for _ in 0..5 {
if rand::random() {
data.push(rand::random::<u64>().to_string());
}
}
for mut s in data {
s.push_str("-edited");
discard(s); // works fine
}
println!("Done");
}
Key takeaways:
'staticbound ≠ data lives forever.- Owned types can be mutated, dropped earlier, and need not exist for the whole execution.
3) &'lt Val Is Not Equivalent to Val: 'lt
&'lt Val implies Val: 'lt, but not vice versa. Val: 'lt is broader:
fn accept_ref<'lt, Val: 'lt>(r: &'lt Val) {}
fn accept_any<'lt, Val: 'lt>(v: Val) {}
struct Wrapper<'lt, Val: 'lt>(&'lt Val);
fn main() {
let s = String::from("text");
accept_any(&s); // ok
accept_any(Wrapper(&s)); // ok
accept_any(&Wrapper(&s)); // ok
accept_ref(&s); // ok
// accept_ref(Wrapper(&s)); // error: expected reference
accept_ref(&Wrapper(&s)); // ok
}
Key takeaways:
Val: 'ltaccepts owned types, nested ref-containing types, and plain references.&'lt Valaccepts only direct references.
4) Lifetime Annotations Are Often Implicit
Rust frequently elides lifetimes via rules:
- Each input reference gets its own lifetime.
- With one input lifetime, it applies to all outputs.
- For methods, if one input is
&self/&mut self, output getsself’s lifetime.
Evenif you don’t write annotations, they are present implicitly:
// Elided form
fn show(txt: &str);
// Expanded form
fn show<'a>(txt: &'a str);
Functions returning a reference without inputs need explicit annotation:
// fn bad() -> &str { "oops" } // error
fn good() -> &'static str { "ok" }
Key takeaways:
- Most Rust code uses implicit lifetimes.
- Even simple functions often rely on elision.
5) Passing Compilation Doesn’t Guarantee Correct Lifetimes
The compiler ensures memory safety, not semantic intent. Example bug due to overly restrictive lifetimes:
struct ByteIter<'data> {
rest: &'data [u8],
}
impl<'data> ByteIter<'data> {
// Buggy: each yielded element tied to 'mut_self
fn next_buggy<'iter>(&'iter mut self) -> Option<&'iter u8> {
if self.rest.is_empty() {
None
} else {
let b = &self.rest[0];
self.rest = &self.rest[1..];
Some(b)
}
}
// Fixed: tie output to 'data, not 'mut_self
fn next_fixed(&mut self) -> Option<&'data u8> {
if self.rest.is_empty() {
None
} else {
let b = &self.rest[0];
self.rest = &self.rest[1..];
Some(b)
}
}
}
Key takeaways:
- Compiler checks safety, not necessarily intended flexibility.
- Meaningful lifetime names expose hidden constraints.
6) Trait Objects Always Have Implied Lifetimes
Trait objects follow elision rules based on context:
use std::cell::Ref;
trait Demo {}
// Box<dyn Demo> => Box<dyn Demo + 'static>
type A = Box<dyn Demo>;
type B = Box<dyn Demo + 'static>;
// &'a dyn Demo => &'a (dyn Demo + 'a)
type C<'a> = &'a dyn Demo;
type D<'a> = &'a (dyn Demo + 'a);
When converting between generic functions and trait-object versions, lifetime differences can appear unexpectedly.
Key takeaways:
- Every
dyn Traitcarries an inferred lifetime. - Context determines whether it’s
'staticor shorter.
7) Error Messages May Suggest Suboptimal Fixes
Compiler suggestions aim for safety, not optimality. Example:
fn first<'a>(x: &'a str, y: &str) -> &str { x }
// Help suggests: -> &'a str for both inputs, overly strict.
// Better: keep second input free:
fn first<'a>(x: &'a str, _y: &str) -> &'a str { x }
Key takeaways:
- Suggestions are safe but not always semantically ideal.
- Understand the actual borrowing needs before applying fixes.
8) Lifetimes Cannot Change at Runtime
Lifetimes are fully static constructs. Attempts to swap references with different lifetimes fail:
struct Holder<'lt> { ref_field: &'lt str }
fn main() {
let long = String::from("long");
let mut h = Holder { ref_field: &long };
{
let short = String::from("short");
h.ref_field = &short; // short dies before h is used again → error
}
}
Even unreachable blocks trigger the same analysis.
Key takeaways:
- Lifetimes fixed at compile time.
- Shortest possible lifetime chosen across all control paths.
9) Downgrading &mut to & via Reborrow Is Tricky
Reborrowing creates a new reference tied to the original’s lifetime:
let mut n = 5;
let shared: &i32 = &*(&mut n); // reborrow
let another: &i32 = &n; // error: n still mutably borrowed
The mutable borrow lives until the shared borrow ends, preventing coexistence.
Key takeaways:
- Avoid reborrowing
&mutto&unless necessary. - Shared reference from reborrow behaves like a mutable borrow in terms of lifetime.
10) Closures Follow Different Lifetime Elision Rules
Closures don’t use the same elision as functions:
fn func(x: &i32) -> &i32 { x } // sugar: fn<'a>(x: &'a i32) -> &'a i32
let closure = |x: &i32| x; // sugar: for<'a,'b> |x: &'a i32| -> &'b i32 { x }
This mismatch can cause confusing errors. Explicit signatures sometimes require boxing or trait objects.
Key takeaways:
- Closure elision differs from functions.
- Be prepared to spell out lifetimes explicitly.
11) &'static T Cannot Always Substitute &'a T in Function Signatures
While 'static references coerce to shorter lifetimes in values, they don’t coerce through generic function boundaries:
fn gen<'a>() -> &'a str { "hello" }
fn stc() -> &'static str { "hello" }
fn pick_val<T>(a: T, b: T) -> T { b } // works with both
fn pick_fn<T, F: Fn() -> T>(a: T, f: F) -> T { f() } // fails if f returns &'static str
Key takeaways:
- Returning
&'static Tis less flexible than a generic&'a Tin higher-order contexts. - Coercion rules differ between value and type-level usage.