Lifetime analysis difference between `&mut` and `ref mut`

I found that the code below works:

pub struct Node<T> {
    pub value: T,
    pub next: Option<Box<Node<T>>>,
}

impl<T> Node<T> {
    pub fn new(value: T) -> Self {
        Self { value, next: None }
    }
}

pub fn push_last_1<T>(head: &mut Node<T>, val: T) {
    let mut cur = head;

    while let Some(ref mut next) = cur.next {
        cur = next;
    }

    cur.next = Some(Box::new(Node::new(val)));
}

However, the following code using &mut fails to compile:

pub fn push_last_2<T>(head: &mut Node<T>, val: T) {
    let mut cur = head;

    while let Some(next) = &mut cur.next {
        cur = next;
    }

    cur.next = Some(Box::new(Node::new(val)));
}

The error message is as follows:

error[E0506]: cannot assign to `cur.next` because it is borrowed
   |
   |     while let Some(next) = &mut cur.next {
   |                            ------------- `cur.next` is borrowed here
...
   |     cur.next = Some(Box::new(Node::new(val)));
   |     ^^^^^^^^
   |     |
   |     `cur.next` is assigned to here but it was already borrowed
   |     borrow later used here

For more information about this error, try `rustc --explain E0506`.

I had always thought that ref mut on the left-hand side was equivalent to &mut on the right-hand side, but it seems that is not the case.

What is the difference that causes the second implementation to fail?

Iā€™m using rustc 1.81.0 with Rust 2021. Additionally, I noticed that the second implementation compiles fine when using RUSTFLAGS="-Zpolonius".

&mut cur.next creates a &mut Option<Node>, not a Option<&mut Node>. Try using cur.next.as_mut() instead to get the equivalent of binding next as ref mut.

Edit: I played around with as_mut(), but I get the same error as with &mut cur.next, the borrow living longer than when we use the ref mut binding.

3 Likes

The difference that matters in this case is that the ref mut does not create the mutable borrow unless the pattern match succeeds, whereas the &mut creates one before the match starts, which is then dropped if the match fails.

In principle, both programs should compile, but the non-Polonius borrow checker has trouble with analyzing conditionally returned borrows, so it is useful to avoid taking a borrow that is not going to be used.

9 Likes

That makes sense. Thanks!!