What & and * Do in Rust Depending on Position: Reading by Reference Depth
While solving one Rust slice problem I got three compile errors. All three had the same cause: I hadn't put & and * in the right places. I only knew & as C's address-of, but in Rust the same symbol works in opposite directions depending on where it appears. Not knowing that made for &i, *v = value and let a = *b all confusing.
This post covers only those two symbols — & and * — and what each does depending on where it appears. It does not cover lifetimes, match ergonomics, or the exclusive-borrow meaning of &mut (shared vs. mutable). I covered the last one in an earlier post; I didn't get stuck on the other two this time, so they're left for later. The get_mut returning an Option that started all this is only the setup here.
Before we start — terms
- expression — a position that computes and produces a value: the right side of
let a = ___, a function argument, an arithmetic expression. Here&creates a reference and*follows one. - pattern — a position that receives and destructures a value: the left side of
let ___ = a,for ___ in, amatcharm. Here&removes one level of reference.*can't be used in a pattern. - reference — the address where a value is stored. You create one with
&x.&i32means "the address of ani32". - dereference — following that address to read the value, written
*p. Same as C's*p. - binding — the
ainlet a = .... You can call it a variable, but strictly it's a name tied to a value. Changing that binding (a = ...) and writing to the location the name points to (*a = ...) are different operations. - reference depth — the measure this post uses: how many
&s are on a type.i32is depth 0,&i32is depth 1,&&i32is depth 2. Instead of memorizing&,*and patterns separately, you only track whether the depth goes +1 or −1.
1. In expressions — same as C
If you know C, there's nothing new here. What confused me was assuming this was all there is.
In expression position, & and * behave exactly as in C. &x produces an address, *p follows that address and reads the value.
let x: i32 = 42;
let p = &x; // C's &x: the address of x
&x type &i32 value(addr) 0x16f336644
x's actual addr 0x16f336644 // identical
*p = 42 // same as C's *p
&i32 is equivalent to C's int*. Rust adds one convenience: when reading, you can often leave the * out. Where C needs *p, you can just write p.
println!("{}", p); // C: printf("%d", *p) — here 42 without the *p
let s = p + 1; // &i32 + i32 works too → 43
This difference — automatic when reading, explicit when writing — is what causes the *v = value problem later, covered in section 5.
2. && is the address of an address
Here I asked, "Is && the address of an address?" — and it is. Only after confirming that did I see that reference depth is the number of times you actually follow an address in memory.
&&x is the address of "the variable holding x's address" — the same as a double pointer (int**) in C.
let x: i32 = 42;
let p: &i32 = &x; // p = &x (address of x)
let pp: &&i32 = &p; // pp = &p (address of where p sits)
where x sits 0x...5e4
what p holds 0x...5e4 ← p ──▶ x
where p sits 0x...5e8
what pp holds 0x...5e8 ← pp ──▶ p
pp ──▶ p ──▶ x
0x5e8 0x5e4 42
Dereferencing follows the addresses one level at a time.
*pp = 0x...5e4 (&i32) one level → back to p (x's address)
**pp = 42 (i32) two levels → x's value
So reference depth is "how many times you follow an address before you reach the value." &&i32 takes two (**pp), &i32 takes one (*p), i32 takes zero. Section 3 builds on this number.
3. Counting depth — let a = b doesn't change it
This is where I was stuck the longest. I couldn't tell let a = b, let a = *b and let a = &b apart. Printing every type made the answer simple: just count the reference depth.
The three forms change depth like this:
| what you write | position | depth |
|---|---|---|
&x | expression | +1 (creates one level of reference) |
*x | expression | −1 (follows one level of reference) |
&p | pattern | −1 (removes one level on the way in) |
And let a = b doesn't change the depth. The right side's type comes through unchanged (±0). That's why I couldn't tell the three apart: = b keeps it, = &b is +1, = *b is −1. Instead of treating them as three different operations, you can tell them apart by how much the depth changes.
Full types, measured (type_name_of_val):
--- starting from b: i32 (depth 0)
let a = b -> i32
let a = &b -> &i32 (+1)
let a = &&b -> &&i32 (+2)
--- starting from r: &i32 (depth 1)
let a = r -> &i32 (±0)
let a = *r -> i32 (−1)
let a = &r -> &&i32 (+1)
The cases that fail are explained by the same counting.
let b: i32 = 7; let a = *b; -> error[E0614]: type `i32` cannot be dereferenced
at depth 0 there is no reference left to follow
let b: i32 = 7; let &a = b; -> error[E0308]: expected `i32`, found `&_`
left side expects depth 1, right side has depth 0
And let *a = b fails before any counting: there is no * syntax in pattern position at all.
let b: i32 = 7; let *a = b; -> error: expected pattern, found `*`
4. & in patterns — the opposite direction from expressions
This is where the problem started. I couldn't work out why for &i in indices has an &. I was reading the i on the left of for as a variable declaration, so &i looked like "declare a reference," which never made sense. On top of that, I knew "&a is accessed with *," and a pattern's & seemed to do the opposite.
Both confusions came from the same misunderstanding: the left side of for or let is a pattern, not a variable declaration. A pattern destructures the incoming value according to its shape.
And there's one reason a pattern's & removes a reference instead of creating one: a pattern writes the expression that built the value in the same shape, and undoes it. Write on the left how the value on the right was built, and the compiler destructures it.
// build (expression) // take apart (pattern)
let e1 = &n; let &p1 = e1; // created with &, so removed with &
let e2 = Some(n); let Some(p2) = e2 else { return };
let e3 = (n, n + 1); let (p3, _) = e3;
So my understanding that "&a is accessed with *" was only true in expression position. Expressions and patterns each do the same −1 with their own syntax.
let a = *r // −1 in an expression
let &a = r // −1 in a pattern → same value
That's why for &i in indices is the same as for i in indices { let i = *i; }. The items of indices.iter() are &usize (depth 1), and get_mut wants a usize (depth 0), so a −1 has to happen somewhere. Do it on the receiving side (pattern) and you get for &i; do it on the using side (expression) and you get get_mut(*i). It's the same calculation written in different places.
5. * on the left of an assignment — changing a binding vs writing a value
This is the real reason my original code was wrong. I had written:
let v = slice.get_mut(i);
v = value; // I believed this would change the slice
I expected v = value to change the element, but the slice stayed the same. v is a binding, and assigning to a binding only changes what the name v refers to; it doesn't touch the original.
v = value and *v = value look alike but do opposite things.
v = value; // rebind the binding v → nothing to do with the original
*v = value; // write where v points → reaches the original
Section 1's "automatic when reading, explicit when writing" matters here. For reads the compiler dereferences for you, but for writes you have to specify the location with * yourself. So a * on the left of an assignment is more accurately read as "specify where to write." Write v = value without the * and the compiler treats it as changing the binding, and the slice stays as it was.
Summary
The correct version of the code I got wrong is these three lines:
for &i in indices { // −1 in a pattern : &usize → usize
if let Some(v) = slice.get_mut(i) { // destructure the Option in a pattern : Option → &mut i32
*v = value; // specify the location in an expression : write to the original
}
}
- In expressions,
&raises reference depth by 1 and*lowers it by 1, same as C. - In patterns,
&lowers reference depth by 1. Patterns have no*. let a = bdoesn't change the depth.- A
*on the left of an assignment specifies where to write. Leave it out and only the binding changes; the original stays the same. - Beyond C's
&and*, I learned two things:&in a pattern works in the opposite direction from an expression, and counting reference depth lets you read all three cases the same way.