Lifetime problem when returning pointer guard through a streaming iterator

I've been trying to push my understanding of lifetime, but I'm hitting this problem with my solution.

Pretty much the general design is that I have some large data in my base state.
I'm trying to generate streaming iterator to list all valid transformation of that state.
It would then return the base state with the transformation applied.
But I would like to return a guard to that state so that when the loop finish using it, the guard to revert the base state back to it's original valid, which then you can use the stream to build the next state.

Here is my minimum design looks like

struct DataGuard<'data> {
    data: &'data mut usize,
}
impl<'data> Drop for DataGuard<'data> {
    fn drop(&mut self) {
        *self.data -= 10000;
    }
}
struct DataStream<'data> {
    data: &'data mut usize,
}

fn steam_entries<'data, 'stream: 'data>(stream: &'stream mut DataStream<'data>) -> DataGuard<'stream> {
    *stream.data += 10000;
    DataGuard { data: stream.data }
}

fn main() {
    let mut data = 0;
    let mut stream = DataStream { data: &mut data };

    loop {
        let guard = steam_entries(&mut stream);
        // ... other statements
        let _ = guard;
    }
}

this returns with the compiler error

error[E0499]: cannot borrow `stream` as mutable more than once at a time
  --> src/main.rs:23:35
   |
23 |         let guard = steam_entries(&mut stream);
   |                                   ^^^^^^^^^^^ mutable borrow starts here in previous iteration of loop

error: aborting due to previous error

My thought process would be that the DataGuard indicated to live as long as DataStream, so that when DataGuard is drop at the end of the loop, there won't be anything holding onto DataStream, and thus it would be safe to borrow DataStream mutably again.

Help in understanding this is appreciated

What made you put in the 'stream: 'data bound?

'stream: 'data is read as “'stream outlives 'data”, on the other hand, the fact that there’s a &'stream mut DataStream<'data> type involved, i.e. (eliminating the “newtype”) a &'stream mut &'data mut usize, means that the 'data livetime must outlive 'stream. Thus there’s an implicit 'data: 'stream constraint in place, too.

In other words, 'stream: 'data in your code forces together with the implicit 'data: 'stream, that 'data == 'stream, i.e. your steam_entries function effectively has the type signature

fn steam_entries<'data>(stream: &'data mut DataStream<'data>) -> DataGuard<'data> 

requiring it’s argument to be exclusively borrowed for its entire lifetime. &'a mut SomeType<'a> is a pretty bad antipattern in Rust in general, it’s the reason why you get an error trying to call stream_entries on the same stream twice: The first call already needed exclusive access for the maximal possible lifetime for which the stream could ever be borrowed.


Long story short: if you just remove the : 'data part, i.e.

- fn steam_entries<'data, 'stream: 'data>(stream: &'stream mut DataStream<'data>) -> DataGuard<'stream>
+ fn steam_entries<'data, 'stream>(stream: &'stream mut DataStream<'data>) -> DataGuard<'stream> {
      *stream.data += 10000;
      DataGuard { data: stream.data }
  }

then your code compiles and behaves as intended, afaict. By the way, if you haven’t already, you should give this a read:


Edit: Some further possibility without actually really changing anything anymore:

  • You can elide the 'data lifetime:
    fn steam_entries<'stream>(stream: &'stream mut DataStream<'_>) -> DataGuard<'stream>
    
  • Perhaps rename the other DataGuard lifetimes for consistency
    struct DataGuard<'stream> {
        data: &'stream mut usize,
    }
    impl<'stream> Drop for DataGuard<'stream> {
        fn drop(&mut self) {
            *self.data -= 10000;
        }
    }
    
  • Here’s a playground with the modification demonstrating that it works.
1 Like

Thanks, removing that lifetime works.
And as you indicated, I've remember the position of the lifetime incorrectly in the generic. Putting that lifetime in the correct order also worked as well.
Worked in both the minimal sample and my actual project.

Yup, I read that a few weeks ago. Inspired me to try this setup with the streaming iterator.

Thanks for your help!