Lifetime issue with generic callback parameter

Hello everyone,

Here is part of the code i'm having issues with : (full code on playground)

struct MyCallback<T: CallbackData> {
    callback: Box<dyn Fn(&T)>,
}

struct MyStruct<T: CallbackData> {
    callbacks: Vec<MyCallback<T>>,
    data: [u8; 3],
}

impl<'a> MyTrait<MyCallbackData<'a>> for MyStruct<MyCallbackData<'a>> {
    fn set_callback(&mut self, cb: MyCallback<MyCallbackData<'a>>) {
        self.callbacks.push(cb);
    }

    fn do_something(&self) {
        let cb_data = MyCallbackData {
            data: &self.data,
        };

        for cb in &self.callbacks {
            (cb.callback)(&cb_data);
        }
    }
}

I'm trying to store multiple callbacks in MyStruct, and call them with a concrete type in the do_something method. Specializing MyStruct (which is parametrized with the argument type of the closure) introduces the following lifetime issue :

cannot infer an appropriate lifetime for borrow expression due to conflicting
requirements

note: ...so that reference does not outlive borrowed content
note: expected `&MyCallbackData<'a>`
	  found `&MyCallbackData<'_>`

I can't seem to find a way to indicate that the reference to cb_data shouldn't live any longer than do_something scope, only long enough to be passed to all the callbacks in the vector.

How could i express this when specializing the struct ? Is there any other better way to do handle this kind of callback mechanism ?

It gets much easier if you don't use temporary borrows in structs:

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=a4ac74f96a2140bd6b58c68613f36aba

(you could also use Arc to avoid copies)

Otherwise you have a problem, because impl<'a> MyTrait<MyCallbackData<'a>> refers to a lifetime that is independent of self (it implies something already existed before self has been created, and the data is in that other place), but {data: &self.data} does depend on self existing and can be guaranteed to be valid only for just this function call, not for the large pre-existing 'a scope.

I don't think it's fixable in the way it's structured. Anything I try with temporarily borrowed data creates weird chicken-egg problems, where callbacks need their data to outlive them, and the struct needs to outlive the callbacks.

Rust also forbids self-referential types. Even though this type isn't technically self-referential, the lifetimes of its types are kinda self-referential (self.callbacks needs to say its type is limited by lifetime of self.data). I don't think if it's possible to declare this generically.

If you have to use temporarily borrowed data, then it's possible to implement this with fewer layers of abstraction:

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=8fa2fd358a960303871625cde6f5a0ff

The key difference is replacing:

Box<dyn Fn(&T)> which can't say that lifetime of T's content can be temporary just for the function call with:

Box<dyn for<'a> Fn(&MyCallbackData<'a>)>

where this for<'a> syntax sort-of creates a new lifetime for every function call (says it's valid for any lifetime).

1 Like

I played with your example a little to get it running with what was in the main function. I hope it's a little closer to what you wanted, the data that your type holds can be borrowed or owned because it uses T, which may or may not be a reference.

I wanted the callback to take a reference that will live only for the duration of the callback without having to use a lifetime generic for the struct. I checked how the standard library does this and they seem to make the type as generic as possible and enforce restrictions only in the impl blocks.

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=d313615d4c6a511145aa064baa2775bc

Thanks a lot for the detailed answer.

Indeed the version without generic does work but it will also causes quite a bit of code duplication in my context. I'm still searching for the best compromise between having lifetime and generic polluting everything or duplicating code. It seems it is a frequent issue i have learning Rust, having a common behavior is not always so simple.

For code duplication there are always macros :slight_smile:

Would it be feasible to replace Vec<Box<dyn Fn(&T)>> with just Vec<U> where U is a user-supplied Box<dyn for<'a> Fn(&ConcreteType<'a>)>?

Also I've noticed you're only using type arguments, and not associated types. Is that intentional? If multiple types are related, they can be grouped together as associated types, and won't have to be explicitly mentioned in so many places.

Thanks, I have actually tried a similar version using closures as generic. But the downside is i can't call set_callback with different closure since every closure has a different type, so it defeats the original idea. That's why i'm trying to use boxed closures.

I guess it would be possible to define a type like this if i understand what you mean, it would indeed be better to do so if i have to reuse it in multiple places.

Indeed, i do intend to use associated types in my real code since i will only have one valid concrete closure argument type per struct, but i just went with generic for this example since both solutions have the same lifetime issue.

Here's the same thing with two different boxed closures being stored in the Vec and taking the same data.

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=2cc53904236e54bb198893d188df9c16

2 Likes

This works but i wouldn't say it is the same thing as now everything is generic. I need to pass a concrete type in the do_something method, hence the specialization in my initial code.

What i need is to to be able to have a method which update a state object (which contains a reference to something in the struct) with a concrete type, and then immediately notifies all the callback passing that state.

If everything is generic, i don't see where i could create my concrete object and pass it to the callback from a MyStruct method.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.