Reputation: 344
I'm looking for a workaround/solution to a problem I've encountered at least a couple times. It occurs when matching to an enum member of a struct, where depending on the match, different (mutating) methods may be called on the struct before the enum's associated value is used. The methods require a mutable reference to the struct, which disallows usage of the enum's associated value afterwards. Trivial example:
struct NonCopyType {
foo: u32
}
enum TwoVariants {
V1(NonCopyType),
V2(NonCopyType)
}
struct VariantHolder {
var: TwoVariants
}
impl VariantHolder {
fn double(&mut self) {
match &mut self.var {
TwoVariants::V1(v) => {
v.foo *= 2;
},
TwoVariants::V2(v) => {
v.foo *= 2;
}
}
}
}
fn main() {
let var = TwoVariants::V1( NonCopyType {
foo: 1
});
let mut holder = VariantHolder {
var
};
match &mut holder.var {
TwoVariants::V1(v) => {
holder.double();
println!("{}", v.foo); // Problem here
},
_ => ()
}
}
Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=5a9f8643546d08878bb5fabe5703d889
This can't be allowed because the variant may have been changed and v
may not even make sense as a value any more. This occurs even if the method does not modify the variant at all; as long as you have to call a mutable method for any reason before using the enum's associated value (say, to mutate a different member which is used in a calculation with the associated value), the borrow checker errors out compilation.
There are two workarounds I can think of. First is to use a second match statement, separating the method calls from the value usage. This I don't like because it separates the logic. Second is to use a nested if let
within the match arm. This is a little better, even if by then I'm 3 tabs deep for what should be a a relatively simple operation.
However, my preferred solution would be to not rematch against the enum at all. Is there a way I can leverage unsafe
to access the enum's associated value without checking the variant? (or any way at all I can avoid rematching after calling a mutating method?)
Upvotes: 4
Views: 1616
Reputation: 494
Compilation of your code yields:
error[E0499]: cannot borrow `holder` as mutable more than once at a time
--> temp.rs:38:13
|
36 | match &mut holder.var {
| --------------- first mutable borrow occurs here
37 | TwoVariants::V1(v) => {
38 | holder.double();
| ^^^^^^ second mutable borrow occurs here
39 | println!("{}", v.foo); // Problem here
| ----- first borrow later used here
So we cannot have 2 mutable borrows. Observe that we are not modifying holder.var
, we can get away with an immutable reference. Change the match &mut holder.var
into match &holder.var
and compile, we get:
error[E0502]: cannot borrow `holder` as mutable because it is also borrowed as immutab
le
--> temp.rs:38:13
|
36 | match &holder.var {
| ----------- immutable borrow occurs here
37 | TwoVariants::V1(v) => {
38 | holder.double();
| ^^^^^^^^^^^^^^^ mutable borrow occurs here
39 | println!("{}", v.foo); // Problem here
| ----- immutable borrow later used here
So the compiler prevents us from performing a mutation (holder.double();
) while the immutable reference (v
of holder.var
) is still in use. Just like you've mentioned,
This can't be allowed because the variant may have been changed and v may not even make sense as a value any more.
However, we, the programmer, have made the following rule: holder.double()
can only modify v
; all other fields should remain the same. E.g. holder.double()
can do v.foo = 13
, but cannot do self.var = TwoVariants::V2(...)
. If the rule is followed, there should be no problem to access v
after calling holder.double()
, since it is still the same v
, only that the v.foo
has changed.
Now the question is, how to access v
after calling holder.double()
?
As pointed out by L. Riemer in the comment, you can use raw pointers with unsafe constructs. Modify the match
expression in main
function to the following code and it should compile:
match &holder.var {
TwoVariants::V1(v) => {
// Create a pointer pointing to v.
let pv = v as *const NonCopyType;
holder.double();
// Dereference the pointer, then create a reference to v.
let v = unsafe { &*pv };
// Access v as usual.
println!("{}", v.foo);
},
_ => ()
}
Note that THIS METHOD IS STRONGLY DISCOURAGED, because the compiler cannot guarantee the validity of the data pointed by pv
at compile time, and there is no runtime error detection too. We just assume that the v
gotten from dereferencing pv
is the original v
and holder.double()
will always follow the rule.
To illustrate the point, try to compile with the modified VariantHolder::double()
:
fn double(&mut self) {
match &mut self.var {
TwoVariants::V1(v) => {
v.foo *= 2;
// Assume that we accidentally perform some operations that modify
// self.var into TwoVariants::V2.
self.var = TwoVariants::V2(NonCopyType { foo: v.foo + 1 });
},
TwoVariants::V2(v) => {
v.foo *= 2;
}
}
}
We can see that it compiles fine. 3
will be printed if you run it, that means v
is actually an element of TwoVariants::V2
after calling holder.double()
, not the original v
anymore.
This kind of bug that compiles fine and produces no runtime error is very hard to spot, pin down and fix. If you add in heap allocations and threads into the system, things will become much more complicated, who knows which operation will break the rule and invalidate pv
.
Recall that our rule only allows modification of v
. One workaround is to utilize the Interior Mutability Pattern with std::cell::RefCell
:
use std::cell::RefCell;
struct NonCopyType {
foo: u32
}
enum TwoVariants {
// Wrap NonCopyType in RefCell, since this is the start of modification
// point allowed by our *rule*.
V1(RefCell<NonCopyType>),
V2(RefCell<NonCopyType>)
}
struct VariantHolder {
var: TwoVariants
}
impl VariantHolder {
// Remove mut, telling the compiler that the `double()` function does not
// need an exclusive reference to self.
fn double(&self) {
match &self.var {
TwoVariants::V1(v) => {
// Borrow mutable from v and modify it.
v.borrow_mut().foo *= 2;
},
TwoVariants::V2(v) => {
v.borrow_mut().foo *= 2;
}
}
}
}
fn main() {
// Create a RefCell to contain NonCopyType.
let var = TwoVariants::V1(RefCell::new(NonCopyType {
foo: 1
}));
let mut holder = VariantHolder {
var
};
match &holder.var {
TwoVariants::V1(v) => {
// Now `double()` only borrow immutably from `holder`, fixing the
// "borrow as mutable while immutable reference is still alive"
// problem.
holder.double();
// Borrow from v.
let v = v.borrow();
// Access v as usual.
println!("{}", v.foo);
},
_ => ()
}
}
In essence we are telling the compiler about our rule, i.e. In the double()
function, holder
, var
, and TwoVarients
are immutable, only v
is mutable.
The advantage of this approach over the unsafe
one is compiler can help us to make sure our rule is followed. Accidental modification in double()
such as self.var = TwoVariants::V2(...)
will result in compile error. RefCell
enforces the borrowing rule at runtime, which will panic!
immediately if violation of the rule occurs.
There are some subtle differences between the RefCell
solution and if let
solution. The if let
solution may look something like this:
match &holder.var {
TwoVariants::V1(v) => {
holder.double();
// Use if-let to unpack and get v from holder.var.
if let TwoVariants::V1(v) = &holder.var {
// Access v as usual.
println!("{}", v.foo);
} else {
panic!("*Rule* violated. Check `holder.double()`.");
}
},
_ => ()
}
RefCell
solution.self.var = TwoVariants::V1(NonCopyType { ... })
inside VariantHolder::double()
, the if let
clause will still extract v
out successfully. However, this extracted v
is no longer the original v
. This fact is important if the NonCopyType
has more than 1 field.Upvotes: 3