Skip to content

← Trait Objects and Dispatch step 8 of 10

Hard Primitives

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

Build two tagger factories: one whose closure borrows a prefix, and one whose closure owns it. Then apply both taggers to the same inputs.

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>)

Each call produces the prefix, a colon, and the input, exactly as format!("{prefix}:{s}") does. Do not trim or otherwise transform either string. Empty prefixes and inputs still get the colon.

run returns results from the borrowing tagger first and the owning tagger second. Preserve input order and duplicates. Both result vectors have the same contents; an empty input vector produces two empty vectors.

Prefix Inputs Each result vector
"log" ["a", "bb"] ["log:a", "log:bb"]
"" ["x", ""] [":x", ":"]
"a:" [":b"] ["a:::b"]

Keep the factory signatures and their compile-time checks. The grader also checks those types independently. Type compatibility does not prove how a closure obtained its data: implement the borrowing factory by retaining the input reference, and the owning factory by retaining the input String.

Why the starter's return type fails

The starter's borrowing factory omits the + '_ bound. In this signature, Box<dyn Fn(&str) -> String> defaults to a 'static object bound. A closure that retains an arbitrary input reference cannot meet that bound.

Adding + '_ asks Rust to infer the object bound from the input lifetime. Here there is one input reference, so the returned type can retain that borrow. This does not extend the prefix's lifetime; callers must keep its data valid while the tagger may use it.

This default is specific to the surrounding type and signature. It is not a rule that every trait object, or every locally inferred box, must be 'static. The default object lifetime rules describe how context changes the inferred bound.

'static does not mean “never dropped”

The owning factory can meet the 'static object bound because its closure owns a String instead of borrowing a caller's local string. The box can still be dropped as soon as the caller finishes with it. The bound does not make it live for the duration of the process or put it in static storage.

A type satisfying 'static may contain owned data or references to static data; it cannot depend on a shorter-lived borrow. In this exercise, moving the owned prefix into the closure gives it the storage it needs.

The prefix borrow and each call argument

The object's lifetime bound concerns what the stored closure captures. The reference passed to each call has its own lifetime. Fn(&str) -> String accepts call arguments with independent lifetimes; the produced String owns its result.

This complete example calls one borrowed tagger with a short-lived input, then calls it again after that input has been dropped:

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

fn main() {
    let prefix = String::from("log");
    let tag = make_tagger(&prefix);
    let first = {
        let input = String::from("temporary");
        tag(&input)
    };
    assert_eq!(first, "log:temporary");
    assert_eq!(tag("next"), "log:next");
}

Writing Fn(&'a str) with the captured prefix's lifetime would remove that independent, any-call-lifetime interface. The starter's MakeTagger alias uses an outer for<'a> for the factory's prefix lifetime; the elided call lifetime inside Fn(&str) is a separate part of the type.

Move the capture, then end the borrow

In the supplied closure expression, move makes the closure capture the prefix variable by value. For the borrowing factory that value is a shared reference, so the underlying string is still borrowed. For the owning factory it is a String, whose ownership moves into the closure. move alone does not turn borrowed data into owned data or make a closure 'static.

In run, finish using and drop the borrowing tagger before moving prefix into make_static_tagger. A nested scope around the first tagger and its result collection makes that ordering clear. The result strings can leave the scope because they own their text.

Read the diagnostic's lifetime relationship

The starter reports that a lifetime may not live long enough. Read which input borrow the compiler is comparing with the return type's requirement. Diagnostic wording and codes depend on the exact program.

A tagger retained after its prefix is dropped is invalid. Similarly, storing make_tagger(&format!("{}", 7)) for use in a later statement borrows a temporary that ends too soon. An immediate call within the same statement can be valid. Bind the prefix to a local variable when the tagger needs it across statements.