@quinedot already gave a good answer, but instead of starting out discussing the “fundamentally forbidden” rules of “aliased &mut
s” which (at least apparently) neither of the versions of the code actually produce, since the two items are different, let me try to outline the borrow checker point of view, as of my understanding.
One important part of basic knowledge is that borrows form a sort of stack. You can have a &mut T
reference, reborrow that for a shorter time to a new &mut T
(or maybe &mut S
…) reference, re-borrow that one again for even shorter; maybe re-borrow it twice, but not at the same time, etc…
So at any given time, with mutable references alone, there’s a stack of borrows from the original variable down to the currently active usable reference that you’ve derived… and if you thing about a whole time-frame, the access-pattern becomes tree-shaped.
// using non-“standard” indentation to emphasize
// the tree-like structure of (mutable) borrows and re-borrows over time
fn main() {
let mut x = 0;
x += 1;
let r1 = &mut x;
*r1 += 1;
let r1_1 = &mut *r1;
*r1_1 += 1;
*r1 += 1;
let r1_2 = &mut *r1;
*r1_2 += 1; // **** (marked line)
*r1 += 1;
x += 1;
let r2 = &mut x;
*r2 += 1;
let r2_1 = &mut *r2;
*r2_1 += 1;
*r2 += 1;
let r2_2 = &mut *r2;
*r2_2 += 1;
*r2 += 1;
x += 1;
dbg!(x); // -> 13
}
By the way, if at any leaf of this tree, the mutable reference is re-borrowed immutably, then such an immutable re-borrow can alias other immutable re-borrows at the same time, giving a tree-shaped (or… “long non-branching (mutable re-borrows) stem, followed by tree of immutable re-borrows, and re-re-borrows, etc…”) borrow pattern even at a single point in time.
fn main() {
let mut y = 0;
let mut x = 0;
x += 1;
let r1 = &mut x;
*r1 += 1;
let r1_1 = &*r1;
let r1_2 = &*r1;
y += *r1_1;
y += *r1_2;
y += *r1_1;
y += *r1_2;
*r1 += 1;
x += 1;
dbg!(x); // -> 4
dbg!(y); // -> 8
}
But without immutable borrows, there is no branching at any given point. E.g. in the // **** (marked line)
above, there is a borrow stack of x
being (mutably) borrowed by r1
, which is re-borrowed into r1_2
, which is then probably even re-borrowed again to create the &mut i32
typed self
argumet passed to the AddAssign
implementation of i32
.
So we have a stack x -> r1 -> r1_2 -> re-borrow of r1_2
at this point of executing the +=
, there are no branches. Still there are notably multiple references existing at the same time; just all but the top-of-stack are sort-of inactive / unusable at the time until their respective re-borrow ends; this is how the “only one mutable reference at a time” kind of rule manifests.
Now onto yet another way of branching… even with only mutable references, branches in this re-borrowing stack/tree are possible, in two ways: One of them is via function calls. If you call a fn(&'a mut X) -> (&'a mut Y, &'a mut Z)
function, then you turned your one mutable reference into two new ones, and you created a branching at the top of your re-borrowing tree. Their lifetimes are connected (they’re the same; but if the function returned e.g. &'b mut Y
with a 'a: 'b
bound, the effect would be the same) so the two new references replace the old one in your mental borrow tree. That’s what happens with the split_at_mut
. Let’s first blow up
let mut v = vec![0,1];
let (f_ref,s_ref) = v.split_at_mut(1);
into more steps…
let mut v = vec![0,1];
let v_ref = &mut v; // line 2
let v_slice_ref = <Vec<_> as std::ops::DerefMut>::deref_mut(v_ref); // the `&mut self` argument for `split_at_mut` // line 3
let (f_ref, s_ref) = <[_]>::split_at_mut(v_slice_ref, 1); // line 3
borrow stack at/after line 2
v <-mut- v_ref
borrow stack at/after line 3
v <-mut- v_slice_ref
and at/after line 4
v <-mut- f_ref
^
|
+--mut- s_ref
So well… now we do have more than one borrow next to each other. But still we cannot create more such borrows directly by accessing v
, of course. Both f_ref
and s_ref
do imply full mutable access over the whole of v
as far as the borrow checker is concerned (the information relevant for safety that, and how, they’re non-overlapping is hidden from the borrow checker behind a function boundary), which means as long as either of f_ref
or s_ref
still exists, you cannot access v
, in particular you cannot directly create new references to v
using syntax such as &v
, until both f_ref
and s_ref
reach the end of their life-time.
Now a short discussion why
let mut v = vec![0,1];
let f_ref = &mut v[0]; // line 2
let s_ref = &mut v[1]; // line 3
(and then accessing f_ref
and s_ref
) fails. The borrow stack after line 2 is
v <-mut- f_ref
which comes from passing a mutable references to v
to the index_mut operation. (It is by the way a general principle that the borrow checker will work on desugared code, so don’t mind me preferring to talk about an index_mut
method rather than indexing syntax. We have a simple fn (&'a mut Vec<T>, usize) -> &'a mut T
method for indexing, and reason about it like any ordinary method call.)
Now the next line, line 3, accesses v
again. Currently f_ref
’s re-borrowing of v
prevents that, so we must “kill” f_ref
(figuratively) and turn the borrow stack back into
v
before being able to execute line 3 resulting in
v <-mut- s_ref
Since f_ref
is “dead” now (figuratively), using f_ref
in a print statement will result in a compilation error.
I eluded to these borrows borrowing v
completely above. There’s also partial borrows. You’d need to make your borrowing stacks/trees even more elaborate to cover those. E.g. if you have a struct foo
, you can create both let ref_a = &mut foo.a
and let ref_a = &mut foo.b
and use them in parallel. Or even borrow on field mutably and another one immutably. I could imagine you might want to write this as
foo <--mut @.a-- ref_a
^
|
+---mut @.b-- ref_b
If foo also has a c
field, it is still not completely borrowed; for every access to foo
while it’s partially borrowed, you would need to consider “what parts of foo
am I trying to access in what manner (mutably vs. immutably)?” and “what parts of foo
are currently borrowed in what manner (mutably vs. immutably)?” and check that the answers to these questions are not involving mutable access to any already borrowed (mutably or immutably) part, and that they don’t involve any access to any part that is already mutably borrowed. Or you’ll need to “kill” existing borrows appropriately.
In fact, the slice patterns that @quinedot demonstrates seem to give the borrow checker a fairly fine-grained view of what parts of the slice are borrowed by what reference. To the point that the following code does compile!
fn main() {
let mut v = vec![0,1];
let slice_ref = &mut v[..];
let (f_ref, s_ref): (&mut i32, &mut i32) = match slice_ref {
[one, two] => (one, two),
_ => unreachable!(),
};
println!("{f_ref:?}{s_ref:?}"); // point A
let f_ref2: &mut i32 = match slice_ref {
[one, _] => one,
_ => unreachable!(),
};
// s_ref is still usable!!
println!("{f_ref2:?}{s_ref:?}");
}
The borrow stack at point A
would need to look something like
v <-mut- slice_ref <-mut @[0]- f_ref
^
|
+--mut @[1]- s_ref
and killing/ending f_ref
makes the index [0]
of the slice accessible again for the new f_ref2
, resulting in the fact that s_ref
is allowed to stay alive throughout the second match
expression, where the second entry is not touched at all because it’s matched against a _
pattern.
See… things are getting complex, and more complex, and more complex… which is why no-one will have a complete mental model of the borrow checker (I needed to test the above code to find out whether or not it would be accepted, I didn’t know myself beforehand, but experimenting with the compiler’s reaction to toy examples is a great way of learning more…), and no-one will apply all their knowledge of the borrow checker to all their code either, especially when code just works.
Anyways, to mention even more… I have not gone into (and admittedly also not a particularly detailed mental model of) what happens if you put references into other structs. In fact, we glanced over the fact that the split_at_mut
function actually returns a single value (&'a mut i32, &'a mut i32)
, at which point one could wonder “how does this compound fit into this borrowing-stack?” before we break it up into two separate &'a mut i32
value by pattern matching. To give some negative answers: “how does this compound fit into this borrowing-stack?” can not be answered with “the borrow checker just considers the individual fields”, since the fields of a struct (in this context, I would consider a tuple to be merely struct
, too) are an implementation detail. If the tuple was a struct Foo<'a>(&'a mut i32, &'a mut i32)
instead, then I’d say: treat the Foo<'a>
like any other reference in your re-borrowing tree. But what if there’s two lifetime parameters, Bar<'a, 'b>(&'a mut i32, &'b mut i32)
. Can we get a borrow-DAG? Probably yes!? But moving a &'a mut i32
field out of Bar<'a, 'b>
would re-instantiate the position of that &'a mut i32
in the part of the borrow-tree where the 'a
lifetime came from. So maybe Bar<'a, 'b>
gets one entry for each of its lifetime parameters… of course a fn foo(&'a mut X, &'b mut Y) -> &'c mut Z
function with 'a: 'c
and 'b: 'c
bounds could now create a single lifetime 'c
that does combine two different original lifetime, so we do get a DAG after all, for sure. I guess it’s getting really really complicated now, so let me just stop… 