Why can a mutable reference exist together with a shared reference but not with another mutable reference?

Consider this case

fn main(){
   let mut v = vec![1,2,3];
   v.push(v.len()); // #1
   v.push(v.pop().unwrap()); // #2
}

The borrow checker can pass #1 but stop at #2. It appears to me they should be identical, that is, both #1 and #2 are ok or are errors. In both cases, the borrowing to v should be ended at the end of the invocation of len or pop, respectively. That is, the borrowings(for len and pop) don't overlap with the mutable borrowing to v for the invocation of push. This thought is based on the argument execution is prior to v.push().

If it is not, that is, the production of mutable borrowing for invocation of push is prior to the argument, then both cases should be an error.

Which thought is right? why #1 is ok but #2 is an error? This example is confusing.

1 Like

This is a weird special case known as two-phase borrows.

The borrows for calling push() do naively conflict because, for any method call, the receiver (implicitly &mut v) is evaluated before the arguments (v.len()).

Two-phase borrows relax this, but the second case is still an error because the rule is that the &mut Vec that is the receiver of push is treated as a shared borrow, not as if it does not exist yet. So it conflicts with another &mut for pop, but not the & for push.

6 Likes

The rationale why v.push(v.len()) is allowed but v.push(v.pop().unwrap()) isn’t is not due to safety concerns, i.e. a not-yet-activated two-phase-borrow aliasing a mutable borrow is not more dangerous than aliasing a shared borrow. The decision to make the not-yet-activated two-phase-borrow act more like a shared borrow than no borrow at all is instead mainly motivated in order to avoid confusion and complexity.

E.g. here’s some excepts from this part of the RFC under “Alternatives”:

// pretend you could define an inherent method on integers
// for a second, just to keep code snippet simple
impl i32 {
    fn increment(&mut self, v: i32) -> i32 {
        *self += v;
        *self // returns new value
    }
}
                                            
fn foo() {
    let mut x = 0;
    let y = x.increment(x.increment(1)); // what result do you expect from this?
    println!("{}", y);
}

The current notion of a 'restricted' borrow is identical to a shared borrow. However, we could in principle permit more things during the restricted period -- basically we could permit anything that does not invalidate the reference we created.
[…]
We opted against this variation for several reasons:

  • It makes the borrow checker more complex by introducing not only two-phase borrows, but a new set of restrictions that must be worked out in detail. The current RFC leverages the existing category of shared borrows.
  • The main gain here is the ability to intersperse two mutable calls (as in the example), or to have an outer shared borrow with an inner mutable borrow. In general, this implies that there is some careful ordering of mutation going on here: in particular, the outer method call will observe the state changes made by the inner calls. This feels like a case where it is helpful to have the user pull the two calls apart, so that their relative side-effects are clearly visible.
5 Likes

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.