Library signatures, thread and task spawning, and callback APIs all hinge on these less obvious lifetime rules.
Lifetimes form a hierarchy: 'b: 'a ("'b outlives 'a") means anything valid for 'b is valid for 'a, so a &'static str can be used where a &'a str is expected. The same syntax applies to types: `T: 'a` means every reference inside T lives at least as long as 'a.
`T: 'static` is the most misread bound. It does not mean the value lives forever. It means T contains no borrowed data shorter than 'static, so the holder may keep it as long as it likes. String, Vec<u8> and Arc<Mutex<X>> are all 'static types even though they are dropped normally. thread::spawn and tokio::spawn require F: 'static because the task may outlive the caller's stack frame; that is why you move owned data in rather than borrowing locals.
Higher-ranked trait bounds say "for every lifetime": F: for<'x> Fn(&'x str) -> &'x str accepts a closure that works on input of any lifetime, rather than one chosen by the caller. You mostly see them implicitly, since Fn(&str) -> &str is sugar for exactly that.
Reborrowing is why passing a &mut T to a function does not move it: the compiler inserts &mut *r, a shorter borrow, and you can use r again afterwards.
In the 2024 edition, a return-position impl Trait automatically captures every generic parameter and lifetime in scope. When a returned iterator should not be tied to some input, opt out with precise capturing: fn ids(&self) -> impl Iterator<Item = u32> + use<> (or list exactly which parameters it uses, e.g. use<'a, T>).
Finally, trait object lifetimes have defaults: Box<dyn Trait> means Box<dyn Trait + 'static>, while &'a dyn Trait means &'a (dyn Trait + 'a). Write Box<dyn Trait + 'a> to store borrowing objects.
Example
use std::fmt::Debug;
// T: 'static means T owns its data (no short borrows), so it may move to another thread
fn spawn_print<T: Debug + Send + 'static>(value: T) -> std::thread::JoinHandle<()> {
std::thread::spawn(move || println!("{value:?}"))
}
// higher-ranked: f must work for whatever lifetime each call gets
fn apply_all<F>(items: &[String], f: F) -> Vec<&str>
where
F: for<'x> Fn(&'x str) -> &'x str,
{
items.iter().map(|s| f(s)).collect()
}
// 2024 edition: the hidden type would capture the borrow of `v`; use<> says it borrows nothing
fn snapshot(v: &[u32]) -> impl Iterator<Item = u32> + use<> { v.to_vec().into_iter() }
fn main() {
spawn_print(String::from("owned is 'static")).join().unwrap();
println!("{:?}", apply_all(&[" a ".into(), " b".into()], str::trim)); // ["a", "b"]
let mut ids = vec![1, 2];
let snap = snapshot(&ids);
ids.push(3); // allowed: snap does not borrow ids
println!("{:?}", snap.collect::<Vec<_>>()); // [1, 2]
}Remember
T: 'staticmeans 'owns its data', not 'lives forever'for<'x>bounds accept callbacks that work for any input lifetime- Rust 2024
impl Traitreturns capture all lifetimes; narrow withuse<..> Box<dyn Trait>defaults to+ 'static
Watch out for
- Trying to satisfy
'staticby leaking or making data global, when cloning orArcwould do.