As all new Rustaceans once went through, I'm currently in my phase of
fighting the borrow-checker. Instead of randomly throwing expedients and
hoping for the best to solve my errors and move on, I would like to deepen my
understanding of Rust and its borrowing techniques, which are fairly new
concepts to me coming from a C and C++ background. I'm looking for solutions
that are efficient, safe, and idiomatic to Rust.
This post contains 5 specific questions marked from (1) to (5).
Here is a minimal reproducible example of my first naive try:
pub struct Round {
hands: ArrayVec<[Hand; 32]>,
// ...
}
impl Round {
pub fn run(mut self) -> RoundResult {
// ...
for hand in self.hands.iter_mut() {
self.do_player_turn(hand);
}
// ...
}
fn do_player_turn(&mut self, hand: &mut Hand) {
// ...
}
}
error[E0499]: cannot borrow `self` as mutable more than once at a time
--> src/round.rs:121:17
|
117 | for hand in self.hands.iter_mut() {
| ---------------------
| |
| first mutable borrow occurs here
| first borrow later used here
...
121 | self.do_player_turn(hand);
| ^^^^ second mutable borrow occurs here
In run()
, self
has ownership of the Round struct: it is not borrowed; so,
calling Round::do_player_turn()
which takes a &mut self
requires a
mutable borrow. Yet, calling ArrayVec<_>::iter_mut()
also requires a
mutable borrow, on self.hands
this time. I suppose that borrowing part of
a struct mutably prevents borrowing the whole struct mutably; this would
create shared mutability over hands
, hence the error on the call to
do_player_turn
. (1) Is my assumption correct?
I understand the need for this restriction: do_player_turn
may operate on
the ArrayVec
instance and invalidate the iterator I'm iterating over. In
my case, the implementation is safe since the only operation that would
create such iterator invalidation would be to remove elements from the array,
something the function never does. But the compiler cannot know that and is
conservative.
Let's try another approach:
impl Round {
pub fn run(mut self) -> RoundResult {
// ...
for i in 0..self.hands.len() {
self.do_player_turn(i);
}
// ...
}
fn do_player_turn(&mut self, i: usize) {
let hand = &mut self.hands[i];
loop {
// ...
self.context.may_double = self.may_double(hand);
// ...
}
}
fn may_double(&self, hand: &Hand) -> bool { false }
}
error[E0502]: cannot borrow `*self` as immutable because it is also borrowed as mutable
--> src/round.rs:169:39
|
165 | let hand = &mut self.hands[i];
| ---------- mutable borrow occurs here
...
169 | self.context.may_double = self.may_double(hand);
| ^^^^ ---- mutable borrow later used here
| |
| immutable borrow occurs here
This error is much more puzzling to me. First, the compiler says line 165
mutably borrows self
while, in my understanding, it was already borrowed by
calling the function since it takes a &mut self
. Without the call to
may_double()
, this code compiles fine; (2) how could it? self
is already
mutably borrowed yet I managed to mutably borrow one of its components (an
ArrayVec
element). (3) Can't we really call a &self
method on a mutably
borrowed struct? What's the rationale?
Let's make an immutable borrow instead:
/* 165 */ let hand = &self.hands[i];
This time, the call to may_double()
works fine. (4) Why? The call requires
an immutable borrow but self
is still mutably borrowed; I'm starting to
think my hypothesis that self
is mutably borrowed in this function is wrong.
Of course it fails a few lines after inside the loop
:
impl Hand {
pub fn add(&mut self, card: Card) { /* ... */ }
}
hand.add(self.shoe.pick());
hand
must be mutated in this function. Perhaps reborrowing it mutably would
help? It also implies to shorten the lifetime of the first borrow and do it
at each iteration:
loop {
let hand = &self.hands[i];
// ...
self.context.may_double = self.may_double(hand);
// ...
let hand = &mut self.hands[i];
// ...
hand.add(self.shoe.pick());
}
This works! It is understandable, by borrowing only for the duration of one
iteration, there is no danger of invalidation, because each call to []
rechecks the validity. Unfortunately, I find this rather ugly and I'm afraid
this will cause measurable performance penalties. Performances are very
important in this project and I would like them to be as close to what I get
in the C++ implementation I'm rewritting it from in Rust.
(5) What would be the efficient and idiomatic way of addressing this issue?
I would like to avoid unsafe
code but I'm willing to rely on it if the safe
alternatives perform poorly.
Thanks for reading so far!
I'm also very eager to read more about this topic if you know any good reference (apart from the Book, of course).