232

Borrow checker rejects a loop that pushes to a vec while reading its last element Help

I have a Vec<Event> and I'm iterating over incoming items, and for each one I want to look at the last element already in the vec to decide whether to merge or push a new one. The compiler tells me I can't borrow events as mutable because it's also borrowed as immutable, which I understand in the abstract but I can't see the shape of the fix. Cloning the last element works but it's a 200 byte struct in a hot loop. What's the normal way to write this?

9 answers Share
Report

Answering anonymously — a moderator will review it first.

  • @borrowck_ben · 2mo ago · 3 replies

    The trick is to end the immutable borrow before you start the mutable one, and the cleanest way is to compute a small decision value inside a scope and then act on it outside. Something like let should_merge = matches!(events.last(), Some(e) if e.can_merge(&item)); then if should_merge { events.last_mut().unwrap().merge(item) } else { events.push(item) }. The last() borrow ends at the end of that statement because the bool you kept doesn't hold a reference, so the last_mut() is free to take a fresh mutable borrow. No clone, no unsafe, and it reads fine.

    214
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @grepwitch · 2mo ago

      The unwrap bugs me slightly. if let Some(last) = events.last_mut() { ... } else { events.push(item) } doesn't work here because of the else branch, so an alternative is to match on should_merge and use events.last_mut().expect("checked above") so the invariant is at least documented.

      78
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
    • @scripting_slowly · 2mo ago

      That's exactly it, and now the rule makes sense: keep a value not a reference. Compiles and the clone is gone.

      52
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @grepwitch · 2mo ago · 2 replies

    Another shape that fits well when the decision needs more than a bool: pull whatever you need out of the last element as owned copies of the small fields, not the whole struct. If the merge decision depends on a timestamp and an id, copy those two, drop the borrow and carry on. You said the struct is 200 bytes but the decision probably needs 16.

    141
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @scripting_slowly · 2mo ago

      It's a u64 and an enum tag, so yes. Both approaches work here, this one is nicer when the condition gets complicated.

      34
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @rackmount_rina · 2mo ago

    If your merge logic gets long, consider an enum for the decision, enum Action { Merge, Push }, computed in one function that takes &[Event] and returns the action. Keeps the borrow inside a function boundary where it obviously ends and makes the loop body two lines.

    84
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @attic_server · 2mo ago

    Worth knowing that this specific pattern is a limitation of the current borrow checker and not a logical problem with your code. It's the classic case people hit and the workaround above is the idiomatic answer, so don't spend an evening thinking you've modelled something wrong.

    96
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @hornworm_hunt · 2mo ago · 2 replies

    Just wrap the vec in a RefCell and the problem goes away.

    26
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @borrowck_ben · 2mo ago

      It moves the same conflict to runtime, where it becomes a panic instead of a compile error, and it costs a check on every access in a hot loop. There's no aliasing need here, so it's strictly worse than restructuring two lines.

      21
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report