Mutable Reference in Rust
Mutable Reference in Rust
Before moving to understanding the mutable reference, let’s have a look at a very basic example in C.
int main() { int i = 1; i = 2;}
This code compiles just fine. Let’s see it’s Rust equivalent.
fn main() { let i:i32 = 1; i = 2;}
This fails with error:
error[E0384]: cannot assign twice to immutable variable `i` --> src/main.rs:3:5 |2 | let i = 20; | - first assignment to `i`3 | i = 2; | ^^^^^ cannot assign twice to immutable variable
Rust treats it’s variables as immutable, unless specified like this:
let mut i:i32 = 1;
This change will now compile the code.
That’s the most basic way to understand the beauty of Rust. It “protects” and “guards” your code, unless specified otherwise. (for the strong-hearted seeunsafe
Not saying that this is not possible in C. Just like Rust, the following C code will fail to compile too. The difference being the use of const
there, mentioned explicitly.
int main() { const int i = 1; i = 2; }
will give the error:
main.c: In function ‘main’:main.c:3:6: error: assignment of read-only variable ‘i’ i = 2; ^
Back to Rust. A mutable reference is a borrow to any type mut T
, allowing mutation of T
through that reference. The below code illustrates the example of a mutable variable and then mutating it’s value through a mutable reference ref_i
.
fn main() { let mut i:i32 = 1; let ref_i:&mut i32 = &mut i; *ref_i = 2;}
Any guesses, what will happen if I add the following line to the code above ?
i = 3;
Yes, it will fail with the error:
error[E0506]: cannot assign to `i` because it is borrowed --> src/main.rs:5:4 |3 | let ref_i:&mut i32 = &mut i; | - borrow of `i` occurs here4 | *ref_i = 2;5 | i=3; | ^^^ assignment to borrowed `i` occurs here
Isn’t that awesome? The error message says it all! Pretty descriptive. No need to scratch your head.
The following C code (more or less, a counterpart of the above Rust example) will compile just fine.
int main() { int i = 1; int* ref_i = &i; *ref_i = 2; i = 3;}
You can mutate the variable i
even when there is an another variable ref_i
(a pointer) pointing to i.
What if we take 2 mutable references?
fn main() { let mut i:i32 = 1; let ref_i = &mut i; let another_ref_i = &mut i;}
This will fail with the error:
error[E0499]: cannot borrow `i` as mutable more than once at a time --> src/main.rs:4:29 |3 | let ref_i = &mut i; | - first mutable borrow occurs here4 | let another_ref_i = &mut i; | ^ second mutable borrow occurs here5 | } | - first borrow ends here
As you can see, it won’t allow another mutable reference while there is already an active mutable reference. Note the “active” here. (see “first borrow ends here
” line in the error message).
Note: Even if the first borrow was immutable, the compiler would still not accept the code.
Now, If I twist the above code a bit and write it the following way, the Rust compiler will happily accept it.
fn main() { let mut i:i32 = 1; { let ref_i = &mut i; } let another_ref_i = &mut i;}
Why? you may ask. Because the mutable referenceref_i
ends when the inner scope ends, allowing the reference another_ref_i
to borrow i
mutably in the outer scope.
So as you see, Rust’s borrow checker is “always angry” at shared mutability. There is another beast called dangling references, which in C can often give you segmentation faults at runtime . That’s where Rust’s lifetimes come to save you and how! Needs a whole separate mega-post to cover lifetimes. For now, let’s keep us focused on mutable references.
Moving ahead, now let’s see the following code:
fn main() { let mut i:i32 = 1; let ref_i = &mut i; let another_ref_i = ref_i;}
This one compiles. But then one may ask, why? as it now seems to have 2 active mutable references ref_i
and another_ref_i
to i
. Shared mutability striking back? Well, no. The mutable reference ref_i
gets moved into another_ref_i
. Mutable references are nonCopy
type. If we add *ref_i = 2
at the end, the compiler will fail with the following error:
error[E0382]: use of moved value: `ref_i` --> src/main.rs:5:4 |4 | let another_ref_i = ref_i; | ------------- value moved here5 | *ref_i = 2; | ^^^^^^^^^^ value used here after move | = note: move occurs because `ref_i` has type `&mut i32`, which does not implement the `Copy` trait
All good! Next example.
fn test (i:&mut i32) {}
fn main() { let mut i:i32 = 1; let ref_i = &mut i; test (ref_i); *ref_i = 2;}
This one compiles. But hold on! Won’t ref_i
get moved into the function test
, rendering it unusable (i.e *ref_i = 2
must fail) after the call?
Well, not really. Apart from moving, the references can also be reborrowed. At the call site to test
, the compiler will insert a reborrow to *ref_i
. test(ref_i)
will be translated to test(&mut *ref_i)
. Roger that! But wait, shouldn’t the reborrow to *ref_i
still prevent *ref_i = 2
? Nope! because at the call site, a temporary scope is created which lasts only till the function test
returns and it is in this scope where the reborrow takes place. At the point where *ref_i = 2
is, apart from ref_i
itself, there is no other reference to i
.
To think of it, if there weren’t reborrows, it would be impossible for mutable references to be used with functions, as they would always be unusable after the call.
Now that we know about reborrows, will the following example compile or not?
fn main() { let mut i:i32 = 1; let ref_i = &mut i; let another_ref_i = &mut *ref_i; //or &*ref_i *ref_i = 2;}
No. Reason being the reborrow to *ref_i
is still active in the scope. There is no inner scope in this case.
The error explains it all!
error[E0506]: cannot assign to `*ref_i` because it is borrowed --> src/main.rs:5:5 |4 | let another_ref_i = &mut *ref_i; //or &*ref_i | ------ borrow of `*ref_i` occurs here5 | *ref_i = 2; | ^^^^^^^^^^ assignment to borrowed `*ref_i` occurs here
Now that we have check-marked a few important points, let’s have a look at the following code (warning: we are entering now into the bewildering world of nested references)
fn main () { let mut i:i32 = 1; let j = { let x = &mut i; let y = &x; &**y };}
As you can see, in the inner scope, y
is dereferenced twice and then reborrowed. Sounds good. Let’s compile the code now.
error[E0597]: `x` does not live long enough --> src/main.rs:6:16 |6 | let y = &x; | ^ borrowed value does not live long enough7 | &**y8 | }; | - `x` dropped here while still borrowed9 | } | - borrowed value needs to live until here
What happened? If we change let x = &mut i
to let x = &i
, it compiles fine. With mutable reference, it fails.
Seems there is more to mutable reference than just being a reference!
As we know, a mutable reference is a non Copy
type, which means x
cannot be copied. But in this case it cannot be moved out as well . Why? because it is behind a reference y
and you cannot move out of a borrowed context. With immutable reference (i.e let x = &i
), there is no question of moving out of borrowed context, as it can be simply copied out.
That’s it! I hope this covered a few important points in using mutable references. As one can see, there are a few scenarios where mutable references can surprise you. But the beauty of Rust is that it guides you with its error messages, trying hard to make you understand what and why the compiler is thinking so. All for your own good. Keeping you safe from the runtime landmines! Having said that, there is still a lot of scope in improving the compiler error messages.