Search code examples
rustlifetimetrait-objects

Inconsistency of lifetime bound requirement when storing closures


When I try to store closures to a HashMap, I come across a lifetime bound requirement reported by the compiler. It seems like an inconsistent requirement.

struct NoBox<C: Fn() -> ()>(HashMap<String, C>);
impl<C> NoBox<C>
where
    C: Fn() -> (),
{
    fn new() -> NoBox<C> {
        NoBox(HashMap::new())
    }
    fn add(&mut self, str: &str, closure: C) {
        self.0.insert(str.to_string(), closure);
    }
}

This is Ok. The compiler is happy with it. However, when I try to wrap the closure into a trait object and store it. The compiler imposes a 'static lifetime bound on it.


struct Boxed(HashMap<String, Box<dyn Fn() -> ()>>);
impl Boxed {
    fn new() -> Boxed {
        Boxed(HashMap::new())
    }
    fn add<C>(&mut self, str: &str, closure: C)
    where
        C: Fn() -> ()//add 'static here fix the error
    {
        self.0.insert(str.to_string(), Box::new(closure)); //error: type parameter C may not live long enough, consider adding 'static lifebound
    }
}

According to the complain of the compiler, C may not live long enough. It makes sense to add a 'static bound to it.

But, why the first case without boxing doesn't have this requirement?

To my understanding, if C contains some reference to an early-dropped referent, then store it in NoBox would also cause the invalid-reference problem. For me, it seems like an inconsistency.


Solution

  • NoBox is not a problem because if the function contains a reference to the lifetime, the type will stay contain this lifetime because the function type needs to be specified explicitly.

    For example, suppose we're storing a closure that captures something with lifetime 'a. Then the closure's struct will looks like (this is not how the compiler actually desugars closures but is enough for the example):

    struct Closure<'a> {
        captured: &'a i32,
    }
    

    And when specifying it in NoBox, the type will be NoBox<Closure<'a>>, and so we know it cannot outlive 'a. Note this type may never be actually explicitly specified - especially with closures - but the compiler's inferred type still have the lifetime in it.

    With Boxed on the other hand, we erase this information, and thus may accidentally outlive 'a - because it does not appear on the type. So the compiler enforces it to be 'static, unless you explicitly specify otherwise:

    struct Boxed<'a>(HashMap<String, Box<dyn Fn() + 'a>>);