Moving a trait fn into a Box<dyn Fn>

Hello there,

I'm trying to move a trait function (Handler::some_fn in the example below) into a separate closure that just wraps the trait function (see Y::bind in example below).

Here is my example code:

use std::rc::Rc;

struct Service;

trait Handler {
    fn some_fn(&self, x: i32);
}

impl Handler for Service {
    fn some_fn(&self, x: i32) {
    }
}

struct Y {
    some_fn: Box<dyn Fn(i32)>,
}

impl Y {
    fn bind<T: Handler>(&mut self, handler: &Rc<T>) {
        let s1 = Rc::clone(&handler);
        self.some_fn = Box::new(
            move |x: i32| {
                s1.some_fn(x);
            }
        );
    }
}

Since I'm using an explicit Rc::clone I would have expected this could be moved into the closure. However I'm getting the following error:

error[E0310]: the parameter type `T` may not live long enough
  --> src/main.rs:21:24
   |
21 |           self.some_fn = Box::new(
   |  ________________________^
22 | |             move |x: i32| {
23 | |                 s1.some_fn(x);
24 | |             }
25 | |         );
   | |_________^ ...so that the type `T` will meet its required lifetime bounds
   |
help: consider adding an explicit lifetime bound...
   |
19 |     fn bind<T: Handler + 'static>(&mut self, handler: &Rc<T>) {
   |                        +++++++++

I don't understand why I need a lifetime, since I would have expected Rc to take care of exactly that. How would I specify a matching lifetime?

Putting a value of type T in an Rc doesn't – can't – change the lifetime annotations of T.

The notation T: 'a means that values of type T only contain references that point to values living at least as long as 'a. Thus, instances of T are valid to hold on to for the lifetime 'a.

This is unchanged by where you move such instances. Putting stuff in an Rc is like owning it (since Rc doesn't borrow anything). But if you own a value and move it somewhere, and that value borrows something, then it still is borrowing the previously-borrowed thing. To the borrow checker, T, Box<T> and Rc<T> have exactly the same ownership structure.

Smart pointers can't keep stuff alive by-reference. Because nothing can keep stuff alive by-reference. References are for borrowing, not for owning. Rc, Arc, Box, and other smart pointer types can't change this, nor can any other type. Thus, wrapping a type in another type can't possibly change the validity of references in the inner type.

There is one more thing that you might be misunderstanding, and which is the direct cause of the compiler error. A trait object is a dynamic type, unknown to the compiler at the points where you actually, usefully interact with it (i.e., at any time after construction). But the compiler always needs lifetimes in order to apply borrow checking and reject invalid code!

However, for most types, the compiler can infer the most important relationships between implicitly-generated and user-annotated lifetimes. Usually, when you use a static type, the compiler can look inside it, and know exactly what is inside, and what you should and should not be allowed to do with instances of that type.

But for a trait object, the specific underlying type it was created from is unknown. The compiler can't possibly "look into" a trait object and check whether it contains any temporary references, because by definition the type is only known at runtime, not at compile time. Thus, you must explicitly add an annotation to the trait object that tells the compiler whether there are temporary references of a given lifetime inside.

There are at least two solutions to your problem:

  1. In function signatures, a bare (owned) dyn Trait is actually implicitly expected to be 'static. Thus, you can slap a 'static bound on your T and call it a day. Demo.
  2. You can propagate any non-'static lifetimes from your handler to your generic type. Demo.

In either case, don't accept an &Rc<T> and clone it. Accepting an explicit, concretely-typed reference-to-smart-pointer is an anti-pattern, and it's almost never what you want. (It can come up in generics accidentally, but here you specifically requested &Rc<T>). You should just accept an Rc<T> by value. The caller can then decide to pass it by-value if they don't need it anymore, or clone it if necessary. With your current signature, it is always cloned, even if it is unnecessary.

4 Likes

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.