2

I want to store a callback that can take different types of parameters (both owned values and references), and can also modify its environment (hence the FnMut). When invoking the callback with a reference, I'd like the compiler to enforce that the parameter is only valid in the closure body. I've tried to implement this using boxed closures.

A minimum example shown below:

fn main() {
    let mut caller = Caller::new();
    let callback = |x: &Foo| println!("{:?}", x);
    caller.register(callback);
    
    let foo = Foo{
        bar: 1,
        baz: 2,
    };
    
    //callback(&foo);       // works
    caller.invoke(&foo);    // borrowed value does not live long enough

}

struct Caller<'a, T> {
    callback: Box<dyn FnMut(T) + 'a>
}

impl<'a, T> Caller<'a, T> {
    fn new() -> Self {
        Caller {
            callback: Box::new(|_| ()),
        }
    }
    
    fn register(&mut self, cb: impl FnMut(T) + 'a) {
        self.callback = Box::new(cb);
    }
    
    fn invoke(&mut self, x: T) {
        (self.callback)(x);
    }
}

#[derive(Debug, Clone)]
struct Foo {
    bar: i32,
    baz: i32,
}

I want to understand why this works if I directly call callback() but the compiler complains about lifetimes if I invoke it through a struct than owns the closure. Perhaps it has something to do with the Box? I can get this to work if I define foo before caller, but I'd like to avoid this.

E_net4
  • 27,810
  • 13
  • 101
  • 139
ger
  • 23
  • 4

1 Answers1

5

This is yet another example of the compiler's type inference quirks when working with closures and bounds of a similar sort (issue #41078). Although this Caller<'a, T> may seem to be well capable of handling invoke calls for a given generic T, the given example passes a reference &'b Foo (where 'b would be some anonymous lifetime of that value). And due to this limitation, T was inferred to be a &Foo of one expected lifetime, which is different from a reference of any lifetime to a value of type Foo (for<'a> &'a Foo), and incompatible with the reference passed to the invoke call.

By not passing the closure to Caller, the compiler would be able to correctly infer the expected parameter type of the callback, including reference lifetime.

One way to overcome this is to redefine Caller to explicitly receive a reference value as the callback parameter. This changes the behavior of the inferred type &T into a higher-ranked lifetime bound, as hinted above.

Playground

fn main() {
    let mut caller = Caller::new();
    let callback = |x: &Foo| { println!("{:?}", x) };
    caller.register(callback);

    let foo = Foo { bar: 1, baz: 2 };

    caller.invoke(&foo);
}

struct Caller<'a, T> {
    callback: Box<dyn FnMut(&T) + 'a>,
}

impl<'a, T> Caller<'a, T> {
    fn new() -> Self {
        Caller {
            callback: Box::new(|_| ()),
        }
    }

    fn register(&mut self, cb: impl FnMut(&T) + 'a) {
        self.callback = Box::new(cb);
    }

    fn invoke(&mut self, x: &T) {
        (self.callback)(x);
    }
}

One way to make this clearer would be to use the expanded definition of invoke:

    fn register<F>(&mut self, cb: F)
    where 
        F: for<'b> FnMut(&'b T) + 'a
    {
        self.callback = Box::new(cb);
    }

See also:

E_net4
  • 27,810
  • 13
  • 101
  • 139
  • Is this a use case for HRTB where we really want to express the `F` bound as `F: for<'b, T: 'b> FnMut(T) + 'a`? – user4815162342 May 11 '21 at 10:05
  • @user4815162342 I suppose that could help, so long as this trait bound would also flow into the receiver type. – E_net4 May 11 '21 at 10:24
  • Is there a way to still allow the callback to receive an owned value as the parameter? – ger May 11 '21 at 17:43