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 its 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 its 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 in Rust)

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 its 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 here
4 | *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 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 here
4 | let another_ref_i = &mut i;
| ^ second mutable borrow occurs here
5 | }
| - 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 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 here
5 | *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) {}

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, Since*ref_i got borrowed by someone else, how can the compiler still allow the code to mutate using *ref_i? Shouldn’t it just prevent this by saying that *ref_i is borrowed ? 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 and goes way with the scope. 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! Only through another_ref_i, can you mutate the value.

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 here
5 | *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 borrowed. *y gives x (mutable reference), which is then again dereferenced and a borrow to the dereferenced mutable borrow is returned.

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 enough
7 | &**y
8 | };
| - `x` dropped here while still borrowed
9 | }
| - 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!

You see, here it is a borrow & to *x ( **y) that gets returned from the inner scope and not just the dereferenced value *x. Hence, along with what the x points to, x itself also needs to live (or get copied out) beyond the inner scope. Think of it like a borrow to a field inside a struct that is behind a reference, being returned. Something like this, which will give the same error.

struct Foo{i:i32}

Coming back, 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. But the borrow & to *x(which is**y) extends beyond the inner scope, leading to a dangling reference situation and hence, the compiler rightfully cries foul. (see dropped here while still borrowed in the error message)

With immutable reference (i.e let x = &i), there is no question of moving out of borrowed context, as for &*x (which is &**y) xcan be simply copied out, along with the borrow that leads it to the outer scope.

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.

Open Source Software Enthusiast, Polyglot & Systems Generalist.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store