Skip to content

← Trait Objects and Dispatch step 8 of 10

Hard Primitives

Trait objects and lifetimes: Box<dyn Trait + 'a>

A factory function that returns a boxed closure is one of the most common shapes in real Rust — configuration builders, parser combinators, callback registries, middleware stacks. It is also the place where the default object lifetime rule bites hardest, and the fix is invisible unless you know the rule.

Write this and it will not compile:

pub fn make_tagger(prefix: &str) -> Box<dyn Fn(&str) -> String> {
    Box::new(move |s| format!("{prefix}:{s}"))
}

Why, precisely

Box<dyn Trait> is shorthand. Every trait object carries a lifetime bound saying how long the erased value may live, and when you leave it out the compiler picks a default. Inside a Box in return position that default is 'static. So you actually wrote:

-> Box<dyn Fn(&str) -> String + 'static>

You then put a closure in it that captured prefix: &str — a borrow with some caller-chosen lifetime that is emphatically not 'static. Contradiction.

The diagnostic is lifetime may not live long enough, and it has no error code. rustc --explain cannot help you; there is nothing to explain. This is one of the genuinely undiagnosable-by-lookup errors in Rust, and knowing to read the message body rather than reach for --explain is part of the skill.

The fix is one token:

pub fn make_tagger(prefix: &str) -> Box<dyn Fn(&str) -> String + '_> { ... }

+ '_ means “tie this trait object to whatever lifetime elision inferred for the inputs” — here, prefix. The returned closure may not outlive the string it borrowed. That is exactly true, and now the compiler knows it.

The two lifetimes, which are unrelated

This is the part worth slowing down for. There are two lifetimes in Box<dyn Fn(&str) -> String + '_> and they have nothing to do with each other.

  1. '_ — how long the closure itself may live, bounded by the borrow of prefix it captured.
  2. the elided lifetime inside Fn(&str) — how long the argument passed to each call must live. That one is universally quantified: the closure accepts a &str of any lifetime, on every call, independently.

Beginners routinely try to unify them and end up writing Box<dyn Fn(&'a str) -> String + 'a>, which compiles but means something much more restrictive: “only ever callable with strings that live as long as the prefix”. This is the first taste of higher-ranked trait bounds — the for<'a> you can see in the MakeTagger type alias in the starter, which says exactly “for every lifetime 'a“.

move is also required. Without it the closure borrows prefix from the function’s own stack frame, which is gone the instant you return.

What to write

The two factories and the entry point:

pub fn make_tagger(prefix: &str) -> Box<dyn Fn(&str) -> String + '_>
pub fn make_static_tagger(prefix: String) -> Box<dyn Fn(&str) -> String>
pub fn run(prefix: String, inputs: Vec<String>) -> (Vec<String>, Vec<String>)

Both taggers produce format!("{prefix}:{s}"). make_static_tagger takes the prefix by value — it owns a String, borrows nothing, and so the 'static default is honest and needs no annotation. That contrast is the whole point: the difference between the two signatures is not style, it is who owns the prefix.

run applies the borrowing tagger to every input to build the first vector, and the owning tagger to build the second. The two vectors are identical in content — the exercise is entirely about making both spellings compile.

The const _: pins at the top of the file assert the signatures. Leave them in. If you change a signature they will stop compiling, which is the point.

Errors you may meet

  • lifetime may not live long enough (no code) — the default 'static.
  • E0597 — a borrowed value does not live long enough; you kept the closure past the prefix.
  • E0716 — a temporary is dropped while still borrowed. make_tagger(&format!("x")) on one line does this.
  • E0621 — an explicit lifetime bound is needed; usually you named one lifetime where you needed two.

Remember the grade is compile + tests + clippy -D warnings.

Loading visualization…