Reborrow vs move

I was trying to understand Non-lexical borrow scopes and better treatment of nested method calls · Issue #811 · rust-lang/rfcs · GitHub and the subsequent comment. I simplified the code, which you can see below:

Playpen: Rust Playground

struct LinkedList {
    val: u64,
    next: Box<LinkedList>,
}

impl LinkedList {
    fn get_index(&mut self, idx: usize) -> u64 {
        let mut node = self;
        for i in 0..idx {
            node = &mut node.next;
        }
        node.val
    }
}

fn main() {}

There are a number of errors, but this is basically what it complains about:

<anon>:10:13: 10:34 error: cannot assign to `node` because it is borrowed [E0506]
<anon>:10             node = &mut node.next;

arielb1 notes in the issue that you can fix this by changing &mut node.next to &mut {node}.next and says this is about reborrow vs move, linking to https://www.reddit.com/r/rust/comments/46rii1/when_to_borrow_move_or_copy/. Unfortunately I'm not finding that link very illuminating, so I figured I'd post here rather than polluting the issue.

In short: what additional information does block brackets around node give the compiler to allow this to work? I'm aware of the usage of blocks to restrict borrows, but I always see that with an expression or multiple statements rather than a single variable name - intuitively, I'd expect {x} to be the same as x. Any help much appreciated!

2 Likes

You might like to read the blog post Stuff the Identity Function Does in Rust. I think this explanation makes this pattern a little bit more clear.

Basically, in Rust, there are a few places where you could either re-borrow or move a particular object. The default that the compiler chooses, of reborrowing, generally works out well. But in some cases, that default isn't actually correct.

When you pass an object into a function or method by value, that moves it into the function, and if you return it it moves it out. A block acts similarly; if you return a value from a block by writing it as the last expression in the block, the value is moved out.

Now, in this case, node is an &mut LinkedList. The default when writing &mut node.next is to reborrow node, so you can still use it later on once the reborrow ends. However, in this case, that causes a problem as you are trying to actually update node, but it's reborrowed by the &mut node.next expression. If you instead wrap node in a block, so the block is returning the value of node, it has to be moved out of instead of reborrowed; and that actually does what you want here, since you are actually just discarding the original reference to node when traversing to the next one.

11 Likes

That's a strange way of using Rust's features and also hacking around the limitation.
&mut {node} this blew my mind, didn't think of this before that...
When I see { something_here } I instantly think of initialization(because of C++).

Interesting, because I would of thought of:

node = {
   node
}

But because of C++, my mind blacks out on node = {node}, thinking of initialization.

I still don't understand why even with {node} it works.
&mut self is a mutable reference. let mut node = selfshould also be a mutable reference(you don't take ownership let mut node = *self)...
Should it not allow you to move out node?
I would expect {node} to throw an error saying that you cannot move it out because it is a reference, you are not the owner of the data. In my mind this moves out self, which I guess I got it wrong.

{node} is not moving the thing node points to, it's moving the pointer itself. &mut T pointers are non-copyable, meaning they can only be moved or re-borrowed.

If Rust didn't do automatic re-borrowing, then any time you did func(some_mut_borrow), then some_mut_borrow would be unusable after the function call. That would be a huge pain in the backside, so the compiler inserts the rough equivalent of func(&mut *some_mut_borrow) for you.

8 Likes

Thanks for the explanation.

This basically explains everything. It's not = *node but = node.

Thanks to everyone for the explanations and links. I realise now that my problem was that I didn't really understand reborrowing or why it's used. I do now!

For future readers, reborrowing has some official documentation at References - The Rustonomicon

(apologies for bumping)

1 Like

I found that the identity function trick doesn't always work:

fn bar(&mut i32) -> &mut i32;

fn foo(mut r: &mut i32) -> &mut i32 {
    for _ in 0 .. 10 {
        r = bar({ r }); // does not work
    }
    r
}

Instead I had to resort to using a dummy let

fn foo(mut r: &mut i32) -> &mut i32 {
    for _ in 0 .. 10 {
        r = bar({ let r = r; r });
    }
    r
}

It would be nice to have a blessed way to do this TBH (maybe create a macro for this?).

The good news is NLL fixes it without having to do any explicit moves at all: Rust Playground

2 Likes

For future readers, reborrowing has some official documentation at https://doc.rust-lang.org/nomicon/references.html#liveness

Sorry for bumping, but the link went dead (archive) and this thread is easier to find than up-to-date documentation. I think the NLL RFC covers liveness and reborrowing now.

1 Like