The borrow checker and loops

I've stumbled over a lifetime issue when borrowing inside a loop.
After minification (keeping the original error message) it looks pretty useless by now. In the original I'm using the Splitter instances to send data from the buffer in packets of a given size.

Given certain circumstances I want to reuse the buffer, put new data into it and for that I create a new Splitter.

I had thought that creating a new Splitter instance would end the borrow, but it's still tracked across loop iterations.

struct Splitter<'a> {
    slice: &'a [u8],
}
impl<'a> Splitter<'a> {
    pub fn has_next(&self) -> bool {
        todo!()
    }
}

struct Builder<'a> {
    pub buffer: &'a mut [u8],
}
impl<'a> Builder<'a> {
    pub fn into_slice(self) -> &'a [u8] {
        self.buffer
    }
}

pub fn demo() -> ! {
    let mut buffer = [0u8; 8];
    let mut splitter_a = Splitter { slice: &[] };
    let mut splitter_b = Splitter { slice: &[] };
    loop {
        // choose which buffer to send...
        let _ = if splitter_a.has_next() {
            &mut splitter_a
        } else {
            // changing this to `&mut splitter_a` will make the code compile?
            &mut splitter_b
        };
        // explicitly overwriting splitter_a does not help
        // splitter_a = Splitter { slice: &[] };
        // neither does dropping (which would have been surprising thinking about
        //    it, since Splitter does not implement Drop so NLL should handle that?)
        // drop(splitter_a);
        let b = Builder {
            buffer: &mut buffer,
        };

        //let slice = b.into_slice();
        splitter_a = Splitter { slice: b.buffer };
    }
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0499]: cannot borrow `buffer` as mutable more than once at a time
  --> src/lib.rs:38:21
   |
26 |         let _ = if splitter_a.has_next() {
   |                    ---------- first borrow used here, in later iteration of loop
...
38 |             buffer: &mut buffer,
   |                     ^^^^^^^^^^^ `buffer` was mutably borrowed here in the previous iteration of the loop

For more information about this error, try `rustc --explain E0499`.
error: could not compile `playground` (lib) due to 1 previous error

Am I correct in thinking that the issue here is that the lifetime of splitter_a is until the end of the function, and the indermediate drop that could happen in the loop is "ignored" by the borrow checker?

If yes, I'd like to understand why changing the second branch of the if to &mut splitter_a let's this compile - I don't see how it makes the situation any better, considering any issues are bound to be linked to splitter_a, and the _ should be dropped after the if?

And also why flat-out replacing splitter_a does not help in this case.

I checked a few sources, but wasn't very successfull in finding a real answer - maybe this helps to see where I'm going wrong:

  • The nomicon does not mention how loops are handled in detail
  • This old thread: Fighting the borrow checker, in a loop seems similar, but confused me a bit because it seems from the responses that it should have been fixed with the 2018 edition, but actually doesn't compile currently?
  • some descriptions on polonious, but those don't seem to apply since they all talk about early returns and named lifetimes.
  • Blog on common lifetime misconceptions, this seems not to apply since the issue persists even if b is dropped before creating a new Splitter (by commenting in the line with into_slice
    (can't post that link anymore, but you can find this here: hxxps://github.com/pretzelhammer/rust-blog/blob/master/posts/common-rust-lifetime-misconceptions.md#9-downgrading-mut-refs-to-shared-refs-is-safe)

Edit: ignore the last point, with into_slice it would make sense as @paramagnetic pointed out.

1 Like

I don’t have any answers, but here is a further-minimized test case in case anyone else finds it useful: Rust Playground

1 Like

My first guess was that splitter_b is inferred to have type Splitter<'static>, and the if expression then causes splitter_a to have the same type. But it seems to fail the same way even if I initialize splitter_b with a non-static buffer.

Response to deleted comment

But then why does it compile successfully if you change both branches of the if expression to be identical?

2 Likes

Ah, right, I missed that.

That's interesting, with those modifications it also fails w/o the loop, if the assignment is repeated twice: Playground
(note that the same behavior with the if is still present, and that I'm not sure this is exactly the same issue, but it seems similar enough - I was just "unrolling" the loop to maybe get an idea)

I think it's something like:[1]

    let mut buffer = [0u8; 8];
    // Splitter<'a>
    let mut splitter_a = Splitter { slice: &mut [] };
    // Splitter<'b>
    let mut splitter_b = Splitter { slice: &mut [] };
    loop {
        // &'foo mut Splitter<'inner>
        // 'inner is invariant
        // 'a must equal 'b must equal 'inner
        let _foo = if true {
            &mut splitter_a
        } else {
            &mut splitter_b
        };

        // Let 'buf be the borrow of the buffer
        // 'buf: 'a
        splitter_a = Splitter { slice: &mut buffer };
    }

'b is alive throughout the loop because there's always a potential use of splitter_b ahead in the control flow (in the else branch), and nothing to kill it. Since _foo forced 'a == 'b, 'a (and 'buf) are also active everywhere in the loop.


Probably we all understand why changing both branches to &mut splitter_b works.

Why does it work when both are splitter_a? I think it's because 'a is dead immediately after the if/else due to the upcoming assignment that overwrites splitter_a. If you just assign the field, the error returns.


  1. I only worked off the minimization of the minimization ↩︎

1 Like

Thanks for the insight - I'll have to rethink how to handle my receive loop for my embedded software then...

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.