Lesson 78 of 94
Advanced Lifetimes
The Lifetimes section covered what you need day to day: annotating functions and structs, elision, 'static, and reading borrow errors. This lesson covers the next layer, the one you meet when reading library signatures or writing generic code:
- Bounds:
T: 'aandT: 'static, and what they actually promise. - Subtyping and variance: when a longer lifetime can stand in for a shorter one, and why
&mutis stricter. - Higher-ranked bounds:
for<'a> Fn(&'a str) -> &'a str. - Hidden lifetimes: in
impl Traitreturn types anddyn Traitobjects. - Lifetimes in traits: lifetime parameters, generic associated types, and lending iterators.
- Reborrowing and
PhantomData.
Part 1: Lifetime bounds
1. T: 'a: the type contains no references shorter than 'a
A generic type T might itself contain references. T: 'a means "every reference inside T lives at least as long as 'a". A struct holding &'a T needs that, and the compiler infers it for you from the field.
use std::fmt::Debug;
// `T: 'a` is implied by the field `&'a T`; writing it out is optional.
struct Labeled<'a, T: 'a> {
label: &'static str,
value: &'a T,
}
fn show<'a, T: Debug + 'a>(item: &Labeled<'a, T>) -> String {
format!("{}: {:?}", item.label, item.value)
}
fn main() {
let owned = vec![1, 2, 3];
let name = String::from("ana");
let borrowed: &str = &name; // T = &str, which contains a reference
println!("{}", show(&Labeled { label: "numbers", value: &owned }));
println!("{}", show(&Labeled { label: "user", value: &borrowed }));
}numbers: [1, 2, 3]
user: "ana"For T = Vec<i32> the bound is trivially true (no references inside). For T = &str it says the inner &str must outlive 'a too.
2. T: 'static means "owns its data", not "lives forever"
T: 'static means T contains no non-'static references. String, Vec<u8>, i32, and Arc<Mutex<X>> all satisfy it, even though a particular String is dropped whenever you like. This is the bound on thread::spawn, tokio::spawn, and Box<dyn Any>.
use std::fmt::Debug;
fn store_forever<T: Debug + 'static>(value: T) -> Box<dyn Debug> {
Box::new(value)
}
fn main() {
let a = store_forever(String::from("owned string")); // String: 'static
let b = store_forever(vec![1, 2, 3]); // Vec<i32>: 'static
let c = store_forever("a literal"); // &'static str: 'static
println!("{a:?} {b:?} {c:?}");
let temp = String::from("dropped soon");
drop(temp); // a 'static *type* can still be dropped at any time
println!("done");
}"owned string" [1, 2, 3] "a literal"
done3. A borrowed value doesn't meet 'static
A reference to a local variable has a lifetime tied to that variable, so it fails a 'static bound:
use std::fmt::Debug;
fn store_forever<T: Debug + 'static>(value: T) -> Box<dyn Debug> {
Box::new(value)
}
fn main() {
let name = String::from("ana");
let boxed = store_forever(&name);
println!("{boxed:?}");
}error[E0597]: `name` does not live long enough
--> src/main.rs:9:31
|
8 | let name = String::from("ana");
| ---- binding `name` declared here
9 | let boxed = store_forever(&name);
| --------------^^^^^-
| | |
| | borrowed value does not live long enough
| argument requires that `name` is borrowed for `'static`
10 | println!("{boxed:?}");
11 | }
| - `name` dropped here while still borrowed
|
note: requirement that the value outlives `'static` introduced here
--> src/main.rs:3:29
|
3 | fn store_forever<T: Debug + 'static>(value: T) -> Box<dyn Debug> {
| ^^^^^^^The fix is usually to pass owned data (name.clone() or name itself), exactly as with thread::spawn.
4. Why 'static bounds show up on tasks and trait objects
A spawned thread or task can outlive the caller, and a Box<dyn Trait> doesn't say how long its contents live. So these APIs demand 'static to be safe. Scoped APIs don't need it, which is why thread::scope can borrow.
use std::thread;
fn main() {
let config = String::from("port=8080");
// spawn: the closure must be 'static, so it must own its data.
let owned = config.clone();
let h = thread::spawn(move || owned.len());
println!("spawn saw {} bytes", h.join().unwrap());
// scope: the closure may borrow, because the scope outlives it.
let n = thread::scope(|s| s.spawn(|| config.len()).join().unwrap());
println!("scope saw {n} bytes, and config is still here: {config}");
}spawn saw 9 bytes
scope saw 9 bytes, and config is still here: port=8080Part 2: Subtyping and variance
5. Longer lifetimes can be used where shorter ones are expected
If 'long: 'short ("'long outlives 'short"), a &'long T can be used anywhere a &'short T is expected. That's subtyping for lifetimes, and it's why a &'static str fits every &str parameter.
fn first_word<'a>(s: &'a str) -> &'a str {
s.split_whitespace().next().unwrap_or("")
}
fn pick<'short>(a: &'short str, b: &'short str) -> &'short str {
if a.len() >= b.len() { a } else { b }
}
fn main() {
let static_str: &'static str = "hello world";
let owned = String::from("rustacean");
// `'static` shrinks to fit the shorter lifetime of `&owned`.
println!("{}", pick(static_str, &owned));
println!("{}", first_word(static_str));
}hello world
hello6. Declaring an outlives relationship: 'b: 'a
You can state that one lifetime parameter outlives another. Then a &'b value can be returned where &'a is promised.
struct Parser<'input> {
text: &'input str,
}
impl<'input> Parser<'input> {
// The result may borrow from `self.text` (lives 'input) or from `fallback` (lives 'f),
// so both must outlive the returned lifetime 'r.
fn field_or<'r, 'f>(&self, name: &str, fallback: &'f str) -> &'r str
where
'input: 'r,
'f: 'r,
{
self.text
.split(';')
.find_map(|kv| kv.strip_prefix(name)?.strip_prefix('='))
.unwrap_or(fallback)
}
}
fn main() {
let p = Parser { text: "host=db.local;port=5432" };
let default_user = String::from("postgres");
println!("{}", p.field_or("host", "localhost"));
println!("{}", p.field_or("user", &default_user));
}db.local
postgresIn practice you rarely need this: one shared lifetime for all inputs usually works, thanks to the subtyping in the previous example.
7. &mut T is stricter: invariance
A &'static str can shrink to a shorter &str. But behind a &mut, lifetimes can't change, because the function could write a short-lived reference into a slot that the caller believes is long-lived. This is called invariance, and the compiler enforces it:
fn overwrite<'a>(slot: &mut &'a str, value: &'a str) {
*slot = value;
}
fn main() {
let mut greeting: &'static str = "hello";
{
let temp = String::from("temporary");
overwrite(&mut greeting, &temp); // would store a reference to `temp`...
} // ...which is dropped here
println!("{greeting}"); // ...and then read here: a dangling reference
}error[E0597]: `temp` does not live long enough
--> src/main.rs:9:34
|
6 | let mut greeting: &'static str = "hello";
| ------------ type annotation requires that `temp` is borrowed for `'static`
7 | {
8 | let temp = String::from("temporary");
| ---- binding `temp` declared here
9 | overwrite(&mut greeting, &temp); // would store a reference to `temp`...
| ^^^^^ borrowed value does not live long enough
10 | } // ...which is dropped here
| - `temp` dropped here while still borrowedIf &mut &'static str could be treated as &mut &'short str, this program would compile and read freed memory.
8. Variance in one table
You rarely need the word "variance", but it explains which conversions the compiler allows:
| Type | Can 'long be used where 'short is expected? |
Why |
|---|---|---|
&'a T |
yes (covariant) | read-only: a longer-lived value is always fine to read |
&'a mut T |
yes for 'a, but not inside T (invariant in T) |
writes through it could store shorter-lived data |
Box<T>, Vec<T>, Option<T> |
yes (covariant) | they own their contents |
Cell<T>, RefCell<T>, Mutex<T> |
no (invariant) | they allow writes through & |
fn(T) |
the reverse (contravariant) | a function accepting any &str also accepts &'static str |
use std::cell::Cell;
fn main() {
let owned = String::from("short-lived");
// Covariant: a Vec<&'static str> can be used as a Vec<&'short str>.
let statics: Vec<&'static str> = vec!["a", "b"];
let mut mixed: Vec<&str> = statics;
mixed.push(&owned);
println!("{mixed:?}");
// Invariant: a Cell<&'static str> stays exactly that type.
let cell: Cell<&'static str> = Cell::new("static");
// cell.set(&owned); // error: `owned` does not live long enough
println!("{}", cell.get());
}["a", "b", "short-lived"]
staticPart 3: Higher-ranked trait bounds
9. A closure that works for *every* lifetime
F: Fn(&str) -> &str looks simple, but it means something precise: for any lifetime 'a, F takes a &'a str and returns a &'a str. Written out, that's for<'a> Fn(&'a str) -> &'a str, a higher-ranked trait bound (HRTB). The elided form is the one you normally write.
fn apply_to_all<F>(inputs: &[String], f: F) -> Vec<&str>
where
F: for<'a> Fn(&'a str) -> &'a str, // same as `F: Fn(&str) -> &str`
{
inputs.iter().map(|s| f(s)).collect()
}
fn trim_brackets(s: &str) -> &str {
s.trim_start_matches('[').trim_end_matches(']')
}
fn main() {
let raw = vec![String::from("[INFO]"), String::from("[WARN]"), String::from("ERROR")];
println!("{:?}", apply_to_all(&raw, trim_brackets));
println!("{:?}", apply_to_all(&raw, |s| s.get(..2).unwrap_or(s)));
}["INFO", "WARN", "ERROR"]
["[I", "[W", "ER"]Why "for every lifetime"? Inside apply_to_all, f is called on references whose lifetimes the caller can't name (they come from inputs.iter()). The bound must promise that f works whatever they turn out to be.
10. Writing for<'a> explicitly
You need the explicit syntax when the lifetime appears somewhere elision can't reach, typically a bound on a reference type. Here, sum_all accepts any collection C such that &C can be iterated, whatever the borrow's lifetime:
use std::collections::{BTreeSet, HashSet, VecDeque};
fn sum_all<C>(collection: &C) -> u64
where
for<'a> &'a C: IntoIterator<Item = &'a u64>,
{
collection.into_iter().sum()
}
fn main() {
println!("{}", sum_all(&vec![1u64, 2, 3]));
println!("{}", sum_all(&VecDeque::from([10u64, 20])));
println!("{}", sum_all(&BTreeSet::from([5u64, 6])));
println!("{}", sum_all(&HashSet::from([100u64])));
}6
30
11
100Read for<'a> &'a C: IntoIterator<Item = &'a u64> as: "however briefly you borrow C, iterating that borrow yields &u64s that live that long".
11. Closures that return references need a hint
Closure signatures are inferred, and the inference doesn't produce "for every lifetime" on its own. A closure that returns a borrow of its argument often fails to compile when stored in a variable:
fn main() {
let first_line = |s: &str| -> &str { s.lines().next().unwrap_or("") };
let text = String::from("line one\nline two");
println!("{}", first_line(&text));
}error: lifetime may not live long enough
--> src/main.rs:2:42
|
2 | let first_line = |s: &str| -> &str { s.lines().next().unwrap_or("") };
| - - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2`
| | |
| | let's call the lifetime of this reference `'2`
| let's call the lifetime of this reference `'1`The fix is to pass the closure through a function whose bound spells out the signature, so inference has something to follow. (A plain fn item also works.)
fn returns_borrow<F: Fn(&str) -> &str>(f: F) -> F {
f
}
fn main() {
let first_line = returns_borrow(|s| s.lines().next().unwrap_or(""));
let text = String::from("line one\nline two");
println!("{}", first_line(&text));
}line onePart 4: Hidden lifetimes in impl Trait and dyn Trait
12. impl Trait captures lifetimes
A function returning impl Iterator<Item = &str> returns something that borrows its input. In Rust 2024, a returned impl Trait automatically captures every lifetime in scope, so this just works:
fn words_longer_than(text: &str, n: usize) -> impl Iterator<Item = &str> {
text.split_whitespace().filter(move |w| w.len() > n)
}
fn main() {
let text = String::from("the quick brown fox jumps over the lazy dog");
let long: Vec<&str> = words_longer_than(&text, 4).collect();
println!("{long:?}");
}["quick", "brown", "jumps"]13. Capturing too much: use<>
Automatic capture is sometimes too conservative. Here the returned iterator doesn't borrow names at all, but because it could, the caller can't modify names while the iterator is alive:
fn numbered(names: &Vec<String>) -> impl Iterator<Item = usize> {
0..names.len()
}
fn main() {
let mut names = vec![String::from("ana"), String::from("ben")];
let ids = numbered(&names);
names.push(String::from("cy"));
println!("{:?} {names:?}", ids.collect::<Vec<_>>());
}error[E0502]: cannot borrow `names` as mutable because it is also borrowed as immutable
--> src/main.rs:8:5
|
7 | let ids = numbered(&names);
| ------ immutable borrow occurs here
8 | names.push(String::from("cy"));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
9 | println!("{:?} {names:?}", ids.collect::<Vec<_>>());
| --- immutable borrow later used here
|
note: this call may capture more lifetimes than intended, because Rust 2024 has adjusted the `impl Trait` lifetime capture rules
--> src/main.rs:7:15
|
7 | let ids = numbered(&names);
| ^^^^^^^^^^^^^^^^
help: use the precise capturing `use<...>` syntax to make the captures explicit
|
1 | fn numbered(names: &Vec<String>) -> impl Iterator<Item = usize> + use<> {
| +++++++A use<...> bound lists exactly what the hidden type captures. use<> means "no lifetimes", which releases the borrow of names as soon as the function returns.
fn numbered(names: &Vec<String>) -> impl Iterator<Item = usize> + use<> {
0..names.len()
}
fn main() {
let mut names = vec![String::from("ana"), String::from("ben")];
let ids = numbered(&names);
names.push(String::from("cy"));
println!("{:?} {names:?}", ids.collect::<Vec<_>>());
}[0, 1] ["ana", "ben", "cy"]14. Box<dyn Trait> means Box<dyn Trait + 'static>
A trait object also hides a lifetime: how long the data behind it lives. When you don't write one, Box<dyn Trait> defaults to 'static. A boxed closure that borrows a local is rejected:
fn make_filters(banned: &[String]) -> Vec<Box<dyn Fn(&str) -> bool>> {
vec![Box::new(move |w| !banned.iter().any(|b| b == w))]
}
fn main() {
let banned = vec![String::from("spam")];
let filters = make_filters(&banned);
println!("{}", filters[0]("hello"));
}error: lifetime may not live long enough
--> src/main.rs:2:5
|
1 | fn make_filters(banned: &[String]) -> Vec<Box<dyn Fn(&str) -> bool>> {
| - let's call the lifetime of this reference `'1`
2 | vec![Box::new(move |w| !banned.iter().any(|b| b == w))]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'static`
|
help: to declare that the trait object captures data from argument `banned`, you can add an explicit `'_` lifetime bound
|
1 | fn make_filters(banned: &[String]) -> Vec<Box<dyn Fn(&str) -> bool + '_>> {
| ++++15. dyn Trait + 'a for borrowing trait objects
Write the lifetime on the trait object to allow it to borrow: Box<dyn Fn(&str) -> bool + 'a>. The '_ shorthand ties it to the function's input.
fn make_filters<'a>(banned: &'a [String], max_len: usize) -> Vec<Box<dyn Fn(&str) -> bool + 'a>> {
vec![
Box::new(move |w| !banned.iter().any(|b| b == w)), // borrows `banned`
Box::new(move |w| w.len() <= max_len), // owns `max_len`: fits any lifetime
]
}
fn main() {
let banned = vec![String::from("spam"), String::from("scam")];
let filters = make_filters(&banned, 6);
let words = ["hello", "spam", "wonderful", "rust"];
let kept: Vec<&str> = words.into_iter().filter(|w| filters.iter().all(|f| f(w))).collect();
println!("{kept:?}");
}["hello", "rust"]The same rule applies to &dyn Trait (default: the reference's own lifetime) and Arc<dyn Trait> (default 'static).
Part 5: Lifetimes in traits
16. A trait with a lifetime parameter
When a trait's methods return data borrowed from an input that isn't self, the trait itself can take a lifetime parameter.
trait Extractor<'text> {
fn extract(&self, text: &'text str) -> Vec<&'text str>;
}
struct Hashtags;
struct Mentions;
impl<'text> Extractor<'text> for Hashtags {
fn extract(&self, text: &'text str) -> Vec<&'text str> {
text.split_whitespace().filter(|w| w.starts_with('#')).collect()
}
}
impl<'text> Extractor<'text> for Mentions {
fn extract(&self, text: &'text str) -> Vec<&'text str> {
text.split_whitespace().filter_map(|w| w.strip_prefix('@')).collect()
}
}
fn run_all<'t>(text: &'t str, extractors: &[&dyn Extractor<'t>]) -> Vec<Vec<&'t str>> {
extractors.iter().map(|e| e.extract(text)).collect()
}
fn main() {
let post = String::from("Loving #rust with @ana and @ben #async");
println!("{:?}", run_all(&post, &[&Hashtags, &Mentions]));
}[["#rust", "#async"], ["ana", "ben"]]Often a method-level lifetime (fn extract<'t>(&self, text: &'t str) -> Vec<&'t str>) is enough; put it on the trait when implementations need to store or name it (for example, in an associated type).
17. An iterator can't return references into itself
Iterator::next(&mut self) -> Option<Self::Item> fixes Item for the whole iterator, so an item can't borrow from the iterator's own buffer for just one call. This is why there's no windows_mut in the standard library. Trying to write one fails:
struct WindowsMut<'a> {
data: &'a mut [i32],
pos: usize,
}
impl<'a> Iterator for WindowsMut<'a> {
type Item = &'a mut [i32];
fn next(&mut self) -> Option<&'a mut [i32]> {
let w = self.data.get_mut(self.pos..self.pos + 2)?;
self.pos += 1;
Some(w) // overlapping windows: two items could alias the same element
}
}
fn main() {}error: lifetime may not live long enough
--> src/main.rs:12:9
|
6 | impl<'a> Iterator for WindowsMut<'a> {
| -- lifetime `'a` defined here
...
9 | fn next(&mut self) -> Option<&'a mut [i32]> {
| - let's call the lifetime of this reference `'1`
...
12 | Some(w) // overlapping windows: two items could alias the same element
| ^^^^^^^ method was supposed to return data with lifetime `'a` but it is returning data with lifetime `'1`The compiler is right: a caller could collect() all windows and hold two &mut to the same element.
18. Generic associated types: a lending iterator
A generic associated type (GAT) is an associated type with its own parameters. type Item<'a> lets each call to next return an item that borrows the iterator for exactly that call. You must drop one item before getting the next, which makes overlapping mutable windows safe.
trait LendingIterator {
type Item<'a>
where
Self: 'a;
fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
}
struct WindowsMut<'s> {
data: &'s mut [i32],
pos: usize,
size: usize,
}
impl<'s> LendingIterator for WindowsMut<'s> {
type Item<'a>
= &'a mut [i32]
where
Self: 'a;
fn next<'a>(&'a mut self) -> Option<&'a mut [i32]> {
let window = self.data.get_mut(self.pos..self.pos + self.size)?;
self.pos += 1;
Some(window)
}
}
fn main() {
let mut prices = [10, 20, 30, 40];
let mut windows = WindowsMut { data: &mut prices, pos: 0, size: 2 };
// Smooth each pair: the second element becomes the average of the pair.
while let Some(w) = windows.next() {
w[1] = (w[0] + w[1]) / 2;
}
println!("{prices:?}");
}[10, 15, 22, 31]while let works because each w is dropped before the next call. collect() wouldn't compile, which is exactly the protection you want.
19. GATs for "a borrowed view of my contents"
GATs also let a trait describe a container that hands out borrowed views of different types per implementation, something a plain associated type can't express.
trait Table {
type Row<'a>
where
Self: 'a;
fn row<'a>(&'a self, i: usize) -> Option<Self::Row<'a>>;
}
struct Csv {
text: String,
}
struct Columns {
names: Vec<String>,
ages: Vec<u32>,
}
impl Table for Csv {
type Row<'a> = Vec<&'a str>; // borrowed fields from the text
fn row<'a>(&'a self, i: usize) -> Option<Vec<&'a str>> {
Some(self.text.lines().nth(i)?.split(',').collect())
}
}
impl Table for Columns {
type Row<'a> = (&'a str, u32); // a borrowed name plus a copied number
fn row<'a>(&'a self, i: usize) -> Option<(&'a str, u32)> {
Some((self.names.get(i)?.as_str(), *self.ages.get(i)?))
}
}
fn main() {
let csv = Csv { text: String::from("ana,34\nben,40") };
let cols = Columns { names: vec![String::from("cy")], ages: vec![29] };
println!("{:?} {:?}", csv.row(1), csv.row(5));
println!("{:?}", cols.row(0));
}Some(["ben", "40"]) None
Some(("cy", 29))Part 6: Reborrowing and markers
20. &mut references move, but calls reborrow
A &mut T isn't Copy: assigning it to another variable moves it. Yet you can pass the same &mut to many function calls in a row. That's because a call implicitly reborrows (&mut *r), creating a shorter-lived &mut and leaving the original usable afterwards.
fn add_item(list: &mut Vec<String>, item: &str) {
list.push(item.to_string());
}
fn main() {
let mut cart = Vec::new();
let r = &mut cart;
add_item(r, "book"); // implicit reborrow: `r` is still usable
add_item(r, "pen");
let moved = r; // plain assignment: `r` is moved into `moved`
add_item(moved, "lamp");
add_item(r, "desk"); // error: `r` was moved
}error[E0382]: borrow of moved value: `r`
--> src/main.rs:12:14
|
7 | let r = &mut cart;
| - move occurs because `r` has type `&mut Vec<String>`, which does not implement the `Copy` trait
...
10 | let moved = r; // plain assignment: `r` is moved into `moved`
| - value moved here
11 | add_item(moved, "lamp");
12 | add_item(r, "desk"); // error: `r` was moved
| ^ value borrowed here after move21. Explicit reborrows
Write &mut *r to reborrow explicitly: for example, to hand a temporary &mut to a generic function (where implicit reborrowing doesn't happen) and keep using r afterwards.
use std::io::Write;
fn write_header<W: Write>(mut out: W) {
writeln!(out, "id,name").unwrap();
}
fn main() {
let mut buf: Vec<u8> = Vec::new();
let r = &mut buf;
write_header(&mut *r); // reborrow: `r` is lent out only for this call
write_header(&mut *r);
r.extend_from_slice(b"1,ana\n"); // `r` is still ours
print!("{}", String::from_utf8(buf).unwrap());
}id,name
id,name
1,anaWith write_header(r), the generic parameter W would become &mut Vec<u8> and take r by value (moving it), so the next line would fail. (&mut W implements Write whenever W does, which is what makes passing &mut *r work.)
22. Two-phase borrows
Some code looks like it borrows twice but compiles: v.push(v.len()) needs &mut v for push and &v for len(). The compiler handles this with two-phase borrows: the &mut is reserved first and only activated after the arguments are evaluated.
fn main() {
let mut ids = vec![10, 20];
ids.push(ids.len()); // two-phase borrow: len() runs before push activates &mut
ids.insert(0, ids[1] - 1);
let mut names = vec![String::from("ana")];
let copy = names[0].to_uppercase(); // compute first...
names.push(copy); // ...then mutate: the general-purpose pattern
println!("{ids:?} {names:?}");
}[19, 10, 20, 2] ["ana", "ANA"]Two-phase borrows only cover method-call autoref like this; for anything more complex, compute the value first, as the bonus line does.
23. PhantomData: a lifetime without a reference
Sometimes a type logically borrows something but doesn't hold a reference to it: an index into a buffer, or a handle into an arena. PhantomData<&'a T> is a zero-sized field that makes the compiler treat the struct as if it held a &'a T, so the borrow checker keeps it from outliving the data.
use std::marker::PhantomData;
struct Arena {
names: Vec<String>,
}
// A handle is just an index, but it's tied to the arena's lifetime.
#[derive(Clone, Copy, Debug)]
struct Handle<'arena> {
index: usize,
_borrows: PhantomData<&'arena Arena>,
}
impl Arena {
fn find(&self, name: &str) -> Option<Handle<'_>> {
let index = self.names.iter().position(|n| n == name)?;
Some(Handle { index, _borrows: PhantomData })
}
fn get(&self, h: Handle<'_>) -> &str {
&self.names[h.index]
}
}
fn main() {
let arena = Arena { names: vec![String::from("ana"), String::from("ben")] };
let h = arena.find("ben").unwrap();
println!("{:?} -> {}", h, arena.get(h));
println!("size of a handle: {} bytes", std::mem::size_of::<Handle>());
// drop(arena); println!("{h:?}"); // error: `arena` is borrowed by `h`
}Handle { index: 1, _borrows: PhantomData<&main::Arena> } -> ben
size of a handle: 8 bytesThe handle costs exactly one usize, yet can't be used after the arena is gone or modified. PhantomData also appears in raw-pointer code (Unsafe Rust & FFI) to tell the compiler what a pointer logically owns or borrows.
24. Any requires 'static
std::any::Any enables runtime downcasting (like instanceof plus a cast). It only works for 'static types, because a type's identity at runtime can't include a lifetime. That's why plugin registries and error downcasting use owned types.
use std::any::Any;
fn describe(value: &dyn Any) -> String {
if let Some(n) = value.downcast_ref::<i32>() {
format!("an i32: {n}")
} else if let Some(s) = value.downcast_ref::<String>() {
format!("a String of length {}", s.len())
} else if let Some(s) = value.downcast_ref::<&'static str>() {
format!("a static str: {s}")
} else {
String::from("something else")
}
}
fn main() {
println!("{}", describe(&42));
println!("{}", describe(&String::from("hello")));
println!("{}", describe(&"literal"));
println!("{}", describe(&vec![1.5]));
}an i32: 42
a String of length 5
a static str: literal
something else25. Realistic: a zero-copy tokenizer behind a trait
A tokenizer trait whose tokens borrow the source text. Implementations for two input formats return the same borrowed token type, and the caller processes tokens without allocating strings.
#[derive(Debug, PartialEq)]
enum Token<'src> {
Ident(&'src str),
Number(&'src str),
Symbol(char),
}
trait Tokenizer {
fn tokens<'src>(&self, src: &'src str) -> Vec<Token<'src>>;
}
struct Simple;
impl Tokenizer for Simple {
fn tokens<'src>(&self, src: &'src str) -> Vec<Token<'src>> {
let mut out = Vec::new();
let mut rest = src;
while let Some(c) = rest.chars().next() {
if c.is_whitespace() {
rest = &rest[c.len_utf8()..];
} else if c.is_alphanumeric() {
let end = rest.find(|ch: char| !ch.is_alphanumeric()).unwrap_or(rest.len());
let (word, tail) = rest.split_at(end);
out.push(if word.chars().all(|ch| ch.is_ascii_digit()) { Token::Number(word) } else { Token::Ident(word) });
rest = tail;
} else {
out.push(Token::Symbol(c));
rest = &rest[c.len_utf8()..];
}
}
out
}
}
fn identifiers<'src>(t: &dyn Tokenizer, src: &'src str) -> Vec<&'src str> {
t.tokens(src).into_iter().filter_map(|tok| if let Token::Ident(s) = tok { Some(s) } else { None }).collect()
}
fn main() {
let source = String::from("total = price * 3 + tax");
println!("{:?}", Simple.tokens(&source));
println!("{:?}", identifiers(&Simple, &source));
}[Ident("total"), Symbol('='), Ident("price"), Symbol('*'), Number("3"), Symbol('+'), Ident("tax")]
["total", "price", "tax"]26. Realistic: an event bus with borrowing listeners
Listeners are boxed closures that borrow local state (+ 'a). The bus can't outlive that state, and the compiler proves every listener is gone before the state is read directly again.
use std::cell::{Cell, RefCell};
#[derive(Debug)]
enum Event<'e> {
Click { x: i32, y: i32 },
Key(&'e str),
}
struct Bus<'a> {
listeners: Vec<Box<dyn Fn(&Event) + 'a>>,
}
impl<'a> Bus<'a> {
fn on(&mut self, f: impl Fn(&Event) + 'a) {
self.listeners.push(Box::new(f));
}
fn emit(&self, e: &Event) {
for l in &self.listeners {
l(e);
}
}
}
fn main() {
let clicks = Cell::new(0);
let typed = RefCell::new(String::new());
{
let mut bus = Bus { listeners: Vec::new() };
bus.on(|e| {
if let Event::Click { .. } = e {
clicks.set(clicks.get() + 1);
}
});
bus.on(|e| {
if let Event::Key(k) = e {
typed.borrow_mut().push_str(k);
}
});
let key_text = String::from("hi");
bus.emit(&Event::Click { x: 1, y: 2 });
bus.emit(&Event::Key(&key_text));
bus.emit(&Event::Click { x: 3, y: 4 });
} // the bus and its borrowing listeners end here
println!("clicks: {}, typed: {:?}", clicks.get(), typed.into_inner());
}clicks: 2, typed: "hi"Fn(&Event) is itself higher-ranked: each listener accepts an event of any lifetime, which is why Event::Key can borrow a string that lives shorter than the bus.
27. Realistic: a string interner with typed symbols
An interner stores each distinct string once and hands out small Symbols. Symbols are plain indices, so they can be copied freely and stored anywhere; resolving one borrows the interner. This design sidesteps the "struct that borrows from itself" problem entirely.
use std::collections::HashMap;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct Symbol(u32);
#[derive(Default)]
struct Interner {
lookup: HashMap<String, Symbol>,
strings: Vec<String>,
}
impl Interner {
fn intern(&mut self, s: &str) -> Symbol {
if let Some(&sym) = self.lookup.get(s) {
return sym;
}
let sym = Symbol(self.strings.len() as u32);
self.strings.push(s.to_string());
self.lookup.insert(s.to_string(), sym);
sym
}
fn resolve(&self, sym: Symbol) -> &str {
&self.strings[sym.0 as usize]
}
}
fn main() {
let mut interner = Interner::default();
let log = "GET /home GET /cart POST /cart GET /home";
let symbols: Vec<Symbol> = log.split_whitespace().map(|w| interner.intern(w)).collect();
println!("{} words, {} distinct strings stored", symbols.len(), interner.strings.len());
let cart = interner.intern("/cart");
let cart_hits = symbols.iter().filter(|&&s| s == cart).count(); // comparing u32s, not strings
println!("{} appears {cart_hits} times", interner.resolve(cart));
}8 words, 4 distinct strings stored
/cart appears 2 timesCompilers (including rustc) intern identifiers exactly like this.
Summary
T: 'ameansT's references outlive'a;T: 'staticmeansTowns its data (no borrowed references), not that it lives forever. Spawning andBox<dyn Trait>require'static; scoped APIs don't.- Lifetimes have subtyping: longer can stand in for shorter (
'long: 'short).&T,Box<T>, andVec<T>allow that conversion;&mut T,Cell<T>, and other mutable containers don't (invariance), which prevents storing short-lived data in long-lived slots. F: Fn(&str) -> &stris a higher-ranked bound,for<'a> Fn(&'a str) -> &'a str. Writefor<'a>explicitly for bounds likefor<'a> &'a C: IntoIterator. Closures returning borrows may need a helper function to get the right signature.impl Traitreturn types capture all lifetimes in scope (Rust 2024);+ use<...>narrows that.Box<dyn Trait>defaults to'static; writedyn Trait + 'ato allow borrowing.- Traits can take lifetime parameters; GATs (
type Item<'a>) enable lending iterators and borrowed views. &mutmoves on assignment but is reborrowed at calls; write&mut *rto reborrow explicitly.PhantomData<&'a T>ties a lifetime to a struct that holds no reference.
Next: Declarative Macros: writing code that writes code with macro_rules!.
