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
call15 | | 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 created13 | 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.)