Search code examples
rustclosureslifetimeborrow-checker

"dropped here while still borrowed" when making lifetime explicits


I am trying to improve my understanding of rust borrow checker by making implicit lifetimes explicit. It actually came from a bigger problem for work, but I boiled it down to this (so far).

Let's take this code as an example :

    struct StringWrapper<'a>(&'a str);
    struct StringWrapperWrapper<'a>(&'a StringWrapper<'a>);

    struct ContainingAValue {
        value: String,
    }

    impl ContainingAValue {
        fn do_something_with_wrapper<F>(&self, f: F)
        where
            F: FnOnce(StringWrapper) -> (),
        {
            let wrapper = StringWrapper(&self.value);
            f(wrapper)
        }

        fn do_something_with_wrapper_wrapper<F>(&self, f: F)
        where
            F: FnOnce(StringWrapperWrapper) -> (),
        {
            let wrapper = StringWrapper(&self.value);
            let tmp = StringWrapperWrapper(&wrapper);
            f(tmp)
        }
    }

This code compiles all right. Now, I want to make the lifetimes explicit in the implementation.

    impl ContainingAValue {
        fn do_something_with_wrapper<'a, F>(&'a self, f: F)
        where
            F: FnOnce(StringWrapper<'a>) -> (),
        {
            let wrapper = StringWrapper(&self.value);
            f(wrapper)
        }

        fn do_something_with_wrapper_wrapper<'a, F>(&'a self, f: F)
        where
            F: FnOnce(StringWrapperWrapper<'_>) -> (),
        {
            let wrapper = StringWrapper(&self.value);
            let tmp = StringWrapperWrapper(&wrapper);
            f(tmp)
        }
    }

Until there, it also compiles all right.

Now, the big question : what lifetime should I put instead of '_ in the StringWrapperWrapper<'_> of do_something_with_wrapper_wrapper ?

I thought that this would work (line number added for reference in the error):

  17   │     fn do_something_with_wrapper_wrapper<'a, F>(&'a self, f: F)
  18   │     where
  19 ~ │         F: FnOnce(StringWrapperWrapper<'a>) -> (),
  20   │     {
  21   │         let wrapper = StringWrapper(&self.value);
  22   │         let tmp = StringWrapperWrapper(&wrapper);
  23   │         f(tmp)
  24   │     }

but I get :

   |
17 |     fn do_something_with_wrapper_wrapper<'a, F>(&'a self, f: F)
   |                                          -- lifetime `'a` defined here
...
21 |         let wrapper = StringWrapper(&self.value);
   |             ------- binding `wrapper` declared here
22 |         let tmp = StringWrapperWrapper(&wrapper);
   |                                        ^^^^^^^^
   |                                        |
   |                                        borrowed value does not live long enough
   |                                        this usage requires that `wrapper` is borrowed for `'a`
23 |         f(tmp)
24 |     }
   |     - `wrapper` dropped here while still borrowed

So, I tried to add a different lifetime :

  17 ~ │     fn do_something_with_wrapper_wrapper<'a: 'b, 'b, F>(&'a self, f: F)
  18   │     where
  19 ~ │         F: FnOnce(StringWrapperWrapper<'b>) -> (),
  20   │     {
  21   │         let wrapper = StringWrapper(&self.value);
  22   │         let tmp = StringWrapperWrapper(&wrapper);
  23   │         f(tmp)
  24   │     }

But get exactly the same error (with 'a being replaced by 'b).

The fact that I am using a FnOnce is important for my usecase and the error as this would compile :

    fn do_something_with_wrapper_wrapper<'a, F>(&'a self, f: F)
    where
        F: FnOnce(StringWrapperWrapper<'a>) -> (),
    {
        let wrapper = StringWrapper(&self.value);
        let tmp = StringWrapperWrapper(&wrapper);
        // f(tmp)
    }

Solution

  • This is a perfect usecase of higher rank trait bounds. The correct code should be

    impl ContainingAValue {
       fn do_something_with_wrapper<'a, F>(&'a self, f: F)
        where
            F: for<'b> FnOnce(StringWrapper<'b>) -> (),
        {
            let wrapper = StringWrapper(&self.value);
            f(wrapper)
        }
        fn do_something_with_wrapper_wrapper<'a, F>(&'a self, f: F)
        where
            F: for<'b> FnOnce(StringWrapperWrapper<'b>) -> (),
        {
            let wrapper = StringWrapper(&self.value);
            let tmp = StringWrapperWrapper(&wrapper);
            f(tmp)
        }
    }
    

    The idea is that you expect f to work no matter the lifetime of the argument, which means in particular it will work with a lifetime bound by the scope of do_something_with_wrapper_wrapper, which cannot be named outside of do_something_with_wrapper_wrapper.

    What you are expressing with

    fn do_something_with_wrapper_wrapper<'a, F>(&'a self, f: F)
    where
        F: FnOnce(StringWrapperWrapper<'a>) -> (),
    {
        let wrapper = StringWrapper(&self.value);
        let tmp = StringWrapperWrapper(&wrapper);
        f(tmp)
    }
    

    is that the caller of do_something_with_wrapper_wrapper chooses the lifetime that f requires for its argument, which may be longer than the scope of do_something_with_wrapper_wrapper.