Ah! The old pointer/pointee issue. Don't worry, it's a common stumbling block. Keep at it, at some point it'll click and then it'll just seem obvious.
What is a reference?
A reference is an indirection. The address of a thing that exists somewhere else.
Let's use an analogy:
- a function frame is a closet, with lots of drawers,
- each drawer may contain one value.
For the moment, let's forget about types, stack and heap: we only have closets and drawers:
- a value is in a drawer, it may moved or copied to another drawer,
- a reference is the address of this value, represented as a pair of IDs (closet ID, drawer ID).
Note: it is nonsensical to mention just a drawer ID, all closets have a drawer 0...
How to use a drawer?
Let us imagine a (typeless) simple example, the addition function:
fn add(left, right) { left + right }
Now, what happens when we call add(3, 4)
?
Function
call
+-add-+ <-- New closet
| 3 | <-- Drawer 0: value 3
+-----+
| 4 | <-- Drawer 1: value 4
+-----+
Note: in the following, I'll represent a closet as an array. This closet would be [3, 4]
. I'll also "number" the closets using letters, to avoid mixing closets and drawers, so this closet could be d
, and the first drawer of d
would be 0@d
(which contains 3 here).
The function is defined as "take the value in drawer 0, take the value in drawer 1, add them together in drawer 2, return content of drawer 2".
Let's spice things up; and introduce references:
fn modify() {
let x = 42;
let y = &x;
*y = 32;
}
What does modify
do?
- call
modify
=> a fresh closet is given to us a: []
,
let x = 42;
=> our closet is now a: [42]
,
let y = &x;
=> our closet is now a: [42, 0@a]
,
And now comes the crux: *y = 32;
. This means put 32 in the drawer pointed to by y
. And thus the closet is now: a: [32, 0@a]
.
Your first example
Let's look at your first example, put in situation with a main
:
// with &mut I can change the referred value of n, but then I can't
// pass a mutable reference anywhere
fn mutate_usize_again(n: &mut usize) {
*n += 1;
// n += 70; ^ cannot use `+=` on type `&mut usize`
}
fn main() {
let mut x = 24;
mutate_usize_gain(&mut x);
}
So, what's going on here?
- call
main
=> a fresh closet is allocated a: []
,
let mut x = 24;
=> a: [24]
,
- call
mutate_usize_again
=> a fresh closet allocated b: []
,
- with
&mut x
=> b: [0@a]
,
*n += 1;
=> add 1 to the drawer pointed to by n
(0@a), this means that b
does not change, but a
does! Now a: [25]
.
n += 70
=> add 70 to a reference... this does not make sense, a reference does not have arithmetic operations.
Your second example
Let's move on to your second example, augmented:
// Parameter renamed because it's confusing to always use the same letter
fn mutate_usize_again(z: &mut usize) { *z += 1; }
fn mutate_usize_two_times(mut n: &mut usize) {
*n = 8;
// if I don't write mut n, I can't pass a mutable reference to
// the mutate_usize_again function
mutate_usize_again(&mut n);
}
fn main() {
let mut x = 24;
mutate_usize_two_times(&mut x);
}
We'll need 3 closets!
- call
main
=> a fresh closet is allocated a: []
,
let mut x = 24
=> a: [24]
,
- call
mutate_usize_two_times
=> a fresh closet is allocated b: []
,
- with
n: &mut usize = &mut x
=> b: [0@a]
,
*n = 8
=> stores 8 in the drawer pointed to by n
: a: [8]
, b
unchanged,
- call
mutate_usize_again
=> a fresh closet is allocated c: []
,
- with
z: &mut usize = &mut n
=> ERROR. The type of &mut n
is &mut &mut usize
which cannot be assigned to a parameter of type z
,
- let's pretend you used
z: &mut usize = n
=> c: [0@a]
,
*z += 1
=> adds 1 to the value in the drawer pointed to by z
: a: [9]
, b
and c
unchanged.
You can take a reference to a reference, but it's not what you meant to do here. We can indeed twist the example to making it work though by defining:
fn mutate_usize_again(z: &mut &mut usize) { **z += 1; }
^ ^~~ dereference TWICE
|~~~~~~~~~~~~~~ two levels of reference
In this case, starting again from the call to mutate_usize_again
:
- before call, we had:
a: [8]
, b: [0@a]
- call
mutate_usize_again
=> a fresh closet is allocated c: []
,
- with
z: &mut &mut usize = &mut n
=> c: [0@b]
<- a reference to a reference!
**z += 1
=> adds 1 to the value in drawer pointed to by the reference pointed to by z
. **z += 1
is **0@b += 1
which is *0@a += 1
, which modifies a
to be a: [9]
and leaves b
and c
unchanged.
Your third example
Your third example, enhanced:
fn mutate_usize_one_time_mutable_pointer(mut n: usize) {
println!("n before assigning in mutate_usize_one_time = {}", n);
n = 48;
println!("n after assigning in mutate_usize_one_time = {}", n);
}
fn main() {
let x = 42;
mutate_usize_one_time_mutable_pointer(x);
}
Let's go:
- call
main
=> a fresh closet is allocated a: []
,
let x = 42;
=> a: [42]
,
- call
mutate_usize_one_time_mutable_pointer
=> a fresh closet is allocated b: []
- with
n: mut usize = x
=> b: [42]
,
n = 48
=> modifies the value of n
: b: [48]
, note that a
is unchanged
- end of
mutate_usize_one_time_mutable_pointer
, discards b
, a: [42]
.
There is no pointer involved here, the name of the function is misleading.
Your final example
Your fourth example, augmented:
fn mutate_usize_one_time(mut n: &usize) {
println!("n before assigning in mutate_usize_one_time = {}", n);
n = &48;
println!("n after assigning in mutate_usize_one_time = {}", n);
}
fn main() {
let x = 42;
mutate_usize_one_time(&x);
}
Let's go:
- call
main
=> a fresh closet is allocated a: []
,
let x = 42;
=> a: [42]
,
- call
mutate_usize_one_time
=> a fresh closet is allocated b: []
,
n: &size = &x
=> b: [0@a]
,
48
=> b: [0@a, 48]
&48
=> b: [0@a, 48, 1@b]
n = &48
=> would lead to b: [1@b, 48]
however lifetimes forbid references pointing to something past them, this is forbidden.
Hope it's starting making sense. If not... then go for a run, go out with friends, clear your head, read another explanation, clear your head again, and come back again. It'll seep in little by little ;)