0

In (an artificial simplification of) my project, I have a struct ContainsData which can only be modified through a handle HoldsRef<'a>, which holds a ref to some field in ContainsData. In addition, after use of the handle, some cleanup method must be called.

struct ContainsData(i64);
impl ContainsData {
    fn cleanup(&mut self) {
        self.0 = self.0.abs();
    }
}

struct HoldsRef<'a>(&'a mut i64);
impl<'a> HoldsRef<'a> {
    fn set(&mut self, value: &'a i64) {
        *self.0 += value; 
    }
}

// A typical interaction with "ContainsData"
fn main() {
    let mut container = ContainsData(5);
    let x = 32;

    let mut handle = HoldsRef(&mut container.0);     // _
    handle.set(&x);                                  // |  A    
    handle.set(&31);                                 // |
    container.cleanup()                              // v
}

I would like to replace A with a single method call to ContainsData. All modifications to the handle will be done in the context of closure as follows:

impl ContainsData {
    fn modify(&mut self, continuation : impl Fn(HoldsRef)) {
        let handle = HoldsRef(&mut self.0); 
        continuation(handle);
        self.cleanup()
    }
}

fn main2() {
    let mut container = ContainsData(5);
    let x = 32;

    container.modify(|mut handle| {
        handle.set(&x);
        handle.set(&32);
    });
}

This does not compile:

error[E0597]: `x` does not live long enough
  --> src/main.rs:56:21
   |
55 |     container.modify(|mut handle| {
   |                      ------------ value captured here
56 |         handle.set(&x);
   |         ------------^-
   |         |           |
   |         |           borrowed value does not live long enough
   |         argument requires that `x` is borrowed for `'static`
...
59 | }
   | - `x` dropped here while still borrowed

First question: why does Rust consider that x is borrowed for 'static? I assume it has to do with the elision rule for Fn(HoldsRef) but I can't quite figure it out what the rule says in that case.

Second question: is there a way to change the signature of modify or alter the code a little bit to be able to replace A with a single method call ? I have tried to annotate lifetimes in various to no avail.

fn modify<'a>(&mut self, continuation : impl Fn(HoldsRef<'a>))
// cannot infer an appropriate lifetime (b/c, I guess, the handle has a lifetime strictly smaller than the function)

Broader context: HoldsRef<'a> is actually wgpu::RenderPass<'a>. I don't have the option to change the signature of the method set.

Ahmad B
  • 117
  • 7

1 Answers1

1

Edit: Found a partial solution below.


Let's understand what's going on here.

Lifetimes in closures (impl Fn(&T), and also impl Fn(T<'_>), which is the same as yours impl Fn(HoldsRef)), do not desugar into generic lifetimes like normal elided lifetimes (where fn foo(_: &T) desugars into fn foo<'a>(_: &'a T)); instead, they're desugared into higher ranked trait bounds.

That is, impl Fn(&T) desugars into impl for<'a> Fn(&'a T), and in the same manner, impl Fn(HoldsRef) desugars into impl for<'a> Fn(HoldsRef<'a>). See also How does for<> syntax differ from a regular lifetime bound?.

HRTB (Higher-Ranked Traits Bounds) means that the lifetime can be any lifetime. And "any lifetime" includes, of course, 'static. And because 'static is the longest lifetime possible, the compiler just use this as if you've written impl Fn(HoldsRef<'static>). Note this is only true from the callback's point of view, that is, the callback must be able to handle this case but modify() can call it with whatever lifetime it wants, not only static (as opposed to what would happen if you'd specify impl Fn(HoldsRef<'static>), then you'd have to pass a HoldsRef<'static>).

Because the HoldsRef::set() method takes &'a i64, and 'a is 'static, so it's like it takes &'static i64. Now you will understand why the compiler complains when you provide a shorter lifetime.

You may think that the solution is just to take a generic lifetime instead of HRTB, and tie it to self:

fn modify<'a>(&'a mut self, continuation: impl Fn(HoldsRef<'a>)) {

However it doesn't work:

error[E0499]: cannot borrow `*self` as mutable more than once at a time
  --> src/main.rs:19:9
   |
16 |     fn modify<'a>(&'a mut self, continuation: impl Fn(HoldsRef<'a>)) {
   |               -- lifetime `'a` defined here
17 |         let handle = HoldsRef(&mut self.0);
   |                               ----------- first mutable borrow occurs here
18 |         continuation(handle);
   |         -------------------- argument requires that `self.0` is borrowed for `'a`
19 |         self.cleanup();
   |         ^^^^^^^^^^^^^^ second mutable borrow occurs here

And this makes sense: since we tell the compiler that the callback takes a HoldsRef<'a>, it can be alive when we cleanup(); and because this HoldsRef borrows self.0, we still hold a mutable reference to self.0 while cleaning up.


Edit: I found a solution! This is not a direct solution to the problem, but rather some kind of workaround over it.

You can impl Drop for HoldsRef, and, in this impl, call cleanup(). That means you'll have to hold a reference to the entire ContainsData and not just its value. Like:

impl ContainsData {
    fn modify<'a>(&'a mut self, continuation: impl Fn(HoldsRef<'a>)) {
        let handle = HoldsRef(self);
        continuation(handle);
    }
}

struct HoldsRef<'a>(&'a mut ContainsData);

impl<'a> HoldsRef<'a> {
    fn set(&mut self, value: &'a i64) {
        self.0.0 += value;
    }
}

impl Drop for HoldsRef<'_> {
    fn drop(&mut self) {
        self.0.cleanup();
    }
}

This also allows you to use RAII-style instead of callback-style, which is usually considered more idiomatic Rust:

impl ContainsData {
    fn get_handle(&mut self) -> HoldsRef<'_> {
        HoldsRef(self)
    }
}

fn main() {
    let mut container = ContainsData(5);
    let x = 32;

    // A block just to make dropping explicit.
    {
        let mut handle = container.get_handle();
        handle.set(&x);
        handle.set(&32);
    }
}
Chayim Friedman
  • 47,971
  • 5
  • 48
  • 77