Why first is not, but second is okay?

use std::rc::Rc;

fn main() {
    let mut all_vec = Vec::new();
    {
        
        let mut this_vec =  Vec::new();
        for i in 0..2 {
            
        
            let a = vec![("ABC".to_string(), 33,), ("ABC".to_string(), 33,)];
            let a= Rc::new(a);
            all_vec.push(a);
            this_vec.push(
                all_vec.last().unwrap()
            );
        }
        
    }
}

This failed:

   Compiling playground v0.0.1 (/playground)
warning: unused variable: `i`
 --> src/main.rs:8:13
  |
8 |         for i in 0..2 {
  |             ^ help: if this is intentional, prefix it with an underscore: `_i`
  |
  = note: `#[warn(unused_variables)]` on by default

error[E0502]: cannot borrow `all_vec` as mutable because it is also borrowed as immutable
  --> src/main.rs:13:13
   |
13 |               all_vec.push(a);
   |               ^^^^^^^^^^^^^^^ mutable borrow occurs here
14 | /             this_vec.push(
15 | |                 all_vec.last().unwrap()
   | |                 -------------- immutable borrow occurs here
16 | |             );
   | |_____________- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
warning: `playground` (bin "playground") generated 1 warning
error: could not compile `playground` (bin "playground") due to previous error; 1 warning emitted

But

use std::rc::Rc;

fn main() {
    let mut all_vec = Vec::new();
    {
        
        let mut this_vec =  Vec::new();
        for i in 0..2 {
            
        
            let a = vec![("ABC".to_string(), 33,), ("ABC".to_string(), 33,)];
            let a= Rc::new(a);
            all_vec.push(a);
            this_vec.push(
                Rc::clone(all_vec.last().unwrap()) // <-- changed only this
            );
        }
        
    }
}

This succeed.

Why? Do the all two codes try to borrow a variable immutable and mutable?

Hey, here's a good tip on asking better question about Rust compiler errors: always provide full error messages if possible. If your IDE doesn't show you full error messages, then run cargo check in a terminal to get the error message from there. The error message will contain most of the information why the compiler sees the problem it sees, such as where the mutable and immutable borrows in question happen, and even in case you don't fully comprehend it yourself, it'll make answering your question here a lot easier for more experienced Rust users that may understand the error message better.

3 Likes

Edited my question. Thanks.

The first major difference between the two versions of the code is the (inferred) type of this_vec.

The code that errors has a this_vec: Vec<&Rc<Vec<(String, i32)>>>, whereas
the code that compiles successfully has a this_vec: Vec<Rc<Vec<(String, i32)>>>.

I wouldn’t know what to comment on why the second code compiles, but let’s discuss the error in the first one, and hopefully it’s clear how the difference meant the same issue doesn’t exist in the compiling code (if not, feel free to ask follow-up questions)

So the main thing of notice here is that this_vec contains references. References in Rust are first-class types so you can put them into data structures such as a Vec. However, even when put into a data structure, they are still subject to the borrow checker.

Let’s follow the borrow checking error… these generally point out two conflicting borrows, and demonstrate that they happen at the same time by saying

  • where does the first borrow start
  • where does the second borrow start/happen
  • where, after that, is the first borrow still used so that we can conclude it must have still been live while the second, conflicting borrow occurred.

I’ll assume you are familiar with the rule a mutable and any other borrow of the same object may not exist at the same time.

Borrows of all_vec happen implicitly here through method calls… the push method is a &mut self method on Vec and means the vector (all_vec) must be mutable borrowed for the push operation.

The last method on Vec (or generally on slices) is a &self method and requires an immutable borrow. Furthermore

pub fn last(&self) -> Option<&T>

it returns another reference (in an Option) that will be considered related to the original reference. To name this relation, you could say it’s “derived” from the &self reference, or perhaps that it’s a “re-borrow”… besides the wording, the important point is the effect: The compiler knows that as long as the &T exists and is live, the original &self borrow of this_vec must stay live, too.

By putting the returned &T (in this case T is Rc<Vec<(String, i32)>> into another vector, we do keep it alive for quite a while… too long in fact. The compiler thus points out:

  • there is an immutable borrow of all_vec created by the last call
    15 | |                 all_vec.last().unwrap()
       | |                 -------------- immutable borrow occurs here
    
    and we put a derived reference into this_vec so this_vec is then also considered borrowing from all_vec (the compiled does not explicitly explain this connection)
  • then, in the next loop iteration there is a mutable borrow of all_vec being created
    13 |               all_vec.push(a);
       |               ^^^^^^^^^^^^^^^ mutable borrow occurs here
    
  • and finally, the immutable borrow from the previous location, which lives on in this_vec, is “used”
    14 | /             this_vec.push(
    15 | |                 all_vec.last().unwrap()
    16 | |             );
       | |_____________- immutable borrow later used here
    
    (this error is “underlining” the entire this_vec.push call; any use of this_vec is considered a use of the borrow of all_vec it contains)

There are multiple factors that make this error message hard to understand

  • the order is a bit reversed because it reasons about multiple loop iterations. The correct order is
    • first: “immutable borrow occurs here
    • then: “mutable borrow occurs here
    • finally: “immutable borrow later used here
  • The connection between the borrow that last creates and the access to this_vec is not explicitly explained.
  • The call to this_vec.push also contains another access to all_vec, so you might be incorrectly deducing the compiler wanted to point at that part of the expression.

There is another angle to look at the error. Instead of understanding the “resoning” of the compiler, we can understand the reason the compiler must complain, i.e. the case of memory unsafety that was prevented; because this is a case (as is relatively often the case) where the error is not just an overly cautious borrow checker (though those cases do also exist), but there would be an actual bug if the erroneous code compiler successfully.

First, we insert a reference (i.e. a pointer) to an element of all_vec into this_vec.

Then we call all_vec.push. Push operations can re-allocate the Vec’s buffer, and if that happens, the reference we took before would become dangling.

The use of this_vec in this_vec.push doesn’t actually access the now-potentially-dangling reference, so from a C-like operational point of view, nothing bad happened yet, but if we were to use this_vec in a way that actually looks at its items later (which for sure was the intention here, anyways), there will be the possibility of dereferencing a dangling pointer.

In this view, it’s somewhat relevant that the reference in the this_vec vector, a reference of type &Rc<Vec<(String, i32)>> pointing to the object in the all_vec vector directly. If it was e.g. instead a &Vec<(String, i32)> reference, to the target of the Rc, then with out C/C++ glasses on, there wouldn’t be any issue anymore, as moving the Rc in a re-allocation wouldn’t invalidate the &Vec<(String, i32)> reference. This kind of reasoning is however not supported by the Rust borrow checker in the first place, so it will conservatively error out even if you tried using &Vec<(String, i32)> instead. There is also the point that this all would be an issue again if other mutations were done to all_vec, such as removing items. There are crates that support Vec-alternatives with additional restrictions, e.g. for Rc<Something> items, you can only extract a &Something reference, not a &Rc<Something> one, and you cannot remove items anymore, and using a Vec-alternative from such a can make code like yours actually compile. (Not that I’m suggesting that that’s the better solution here, cloning the Rc sounds like a very reasonable approach, too.)

3 Likes

Thank you for very detailed answer.
I appreciate that!