We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Ownership III: Lifetimes, Explicitly step 17 of 22
`T: 'static` in practice — a type-erased handler registry
Two registries that hold boxed closures. They differ by five characters, and those five characters are the difference between an API that composes and one that fights every caller.
pub type Handler = Box<dyn Fn(i64) -> i64>;
pub type BorrowedHandler<'a> = Box<dyn Fn(i64) -> i64 + 'a>;
pub struct Registry { handlers: Vec<Handler> }
pub struct BorrowingRegistry<'a> { handlers: Vec<BorrowedHandler<'a>> }
Each gets register and call_all(&self, x) -> Vec<i64>, which runs every
handler on x in registration order. Then:
pub fn run(offsets: Vec<i64>, x: i64) -> (Vec<i64>, Vec<i64>)
builds one of each. For every offset it registers a handler computing
x.wrapping_add(offset) — but the owning registry’s closures capture an
owned i64 copy, and the borrowing registry’s closures capture a
&i64 pointing into offsets. Both tuples come out identical. The
arithmetic is not the exercise.
The two meanings of 'static
This is the single most misread thing in Rust, so get it straight before you write a line:
-
&'static T— a reference whose referent is valid for the whole program. String literals,consts,statics,Box::leak. -
T: 'static— a bound on a type, meaning “this type contains no references that could become invalid”. Every owned type satisfies it.String: 'static.Vec<u8>: 'static.i64: 'static. AStringyou allocate at noon, mutate at 12:01 and drop at 12:02 satisfiesT: 'staticthe entire time.
It does not mean “lives forever”. It means “does not borrow anything
short-lived”. Learners who read F: Fn(i64) -> i64 + 'static as “F lives
forever” conclude they can never spawn a thread over their own data, and lose
an afternoon.
Where the trap is hidden
Look again at Vec<Box<dyn Fn(i64) -> i64>>. There is no lifetime in it. There
is also no warning about it — and it silently means:
Vec<Box<dyn Fn(i64) -> i64 + 'static>>
Trait objects have default lifetime bounds, and because Box<T> puts no
lifetime bound on T, the default is 'static. That default is exactly right
for Registry, whose whole point is to hold handlers that outlive whatever
built them. It is exactly wrong for BorrowingRegistry, and nothing tells you.
Get the field type wrong there and the implementation is what breaks:
error[E0310]: the parameter type `F` may not live long enough
the parameter type `F` must be valid for the static lifetime
The alias BorrowedHandler<'a> is given to you already correct. The bound on
BorrowingRegistry::register is not. The starter says + 'static on both
registries; only one of them should.
The pin
const _: () = {
fn _pin<'a>(r: &mut BorrowingRegistry<'a>, table: &'a [i64]) {
r.register(move |x| x.wrapping_add(table[0]));
}
};
A registry parameterised by 'a, a table borrowed for 'a, and a closure that
captures the table. That must work — it is the entire reason
BorrowingRegistry exists. With the starter’s + 'static bound you get:
error[E0521]: borrowed data escapes outside of function
argument requires that `'a` must outlive `'static`
Read that last line slowly. You asked for a closure that borrows nothing
short-lived; the caller handed you one that borrows table; the only way to
reconcile them is for 'a to be 'static, which it is not. The fix is
register<F: Fn(i64) -> i64 + 'a> — “F may borrow, as long as it borrows for at
least as long as this registry does.”
Do not edit or delete the pin.
Small things that matter
-
type_complexityis a warn-by-default clippy lint andVec<Box<dyn Fn(i64) -> i64>>is close to its threshold. The two type aliases are there so you never have to find out; use them. -
The site compiles with
-O, so overflow checks are off anddebug_assertionsis false. A plain+would wrap silently rather than panic. Usewrapping_addso the behaviour is a decision instead of an accident — two of the tests sit exactly oni64::MIN/i64::MAX. -
RegistryandBorrowingRegistryboth deriveDefault; build them with::default()rather than writing anew(clippy::new_without_defaultwould object to an argument-lessnewwith noDefaultimpl). -
To make the borrowing closures actually borrow, iterate
&offsetsand let each closuremovea&i64into itself.moveon a reference copies the reference, which is still a borrow — that is the whole point.Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.