The first major difference between the two versions of the code is the (inferred) type of
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.
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.
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
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
and we put a derived reference into
15 | | all_vec.last().unwrap()
| | -------------- immutable borrow occurs here
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”
(this error is “underlining” the entire
14 | / this_vec.push(
15 | | all_vec.last().unwrap()
16 | | );
| |_____________- immutable borrow later used here
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
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.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.)