0

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.

Tim
  • 99
  • 1
  • 5
  • 1
    Nitpick: `Fn() -> ()` is just `Fn()`. – Chayim Friedman Aug 21 '22 at 12:49
  • 1
    A large part of the confusion stems from `Box` being short for `Box`. On the other hand, `impl Trait` has no such implied `'static` lifetime, which leads to the perceived impedance mismatch. – user4815162342 Aug 21 '22 at 17:39
  • Thanks for the answer and comments, very helpful. I'd think it is that closures are of anonymous types which aren't explicitly spelled out and are implicitly inferred by the compiler. As a result, it is easy to neglect the fact that the lifetime constraint is entailed in the constructed type. And when it comes to trait objects, type inference can't play a part. The compiler then asks for the lifetime bound, which is a surprise for ignorant like me :) – Tim Aug 22 '22 at 02:48

1 Answers1

3

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>>);
Chayim Friedman
  • 47,971
  • 5
  • 48
  • 77