Counting Layers: What & and * Actually Do in Rust, Depending on Where They Sit
I was working through a Rust slice exercise when the compiler yelled at me three times. But the three errors turned out to wear the same face — every one of them was me putting & or * in the wrong place. I thought & just handed back an address the way it does in C. In Rust, though, the same character works in opposite directions depending on where it sits. Until that clicked, for &i, *v = value, and let a = *b were all a blur.
This post covers exactly those two symbols — & and * — and what each does depending on its position. It does not cover lifetimes, match ergonomics, or the exclusive-borrow meaning of &mut (shared vs. mutable). I dug into the last one in an earlier post; the other two I never got stuck on this time, so they wait for next time. The get_mut returning an Option that kicked this all off is just the setup here, nothing more.
Before we start — terms
- Expression — a spot that computes and produces a value. The right side of
let a = ___, function arguments, arithmetic. Here&builds a reference and*follows one. - Pattern — a spot that receives and destructures a value. The left side of
let ___ = a,for ___ in, the arms ofmatch. Here&strips a reference. And*doesn't exist at all. - Reference — the address where some value lives. You make one with
&x.&i32is "the address where ani32sits." - Dereference — following that address to reach the value. You do it with
*p. Same as C's*p. - Binding — the
ainlet a = .... You can call it a "variable," but precisely it's a name tied to a value. Rebinding the name (a = ...) and writing through where the name points (*a = ...) are two different acts. - Layers / counting layers — the tool this post runs on. How many
&s are stacked on a type.i32is 0 layers,&i32is 1,&&i32is 2. Instead of memorizing&/*/patterns separately, you just count +1/−1 on the layers.
1. In expression position — same as C
If you know C, there's nothing new here. My confusion wasn't with this part — it was assuming this part was the whole story.
In expression position, & and * behave exactly like C. &x makes an address; *p follows it to fetch 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 int*, no more and no less. There's exactly one convenience Rust adds: when reading, you can often drop the *. Where C forces you to write *p, Rust lets p slide.
println!("{}", p); // C: printf("%d", *p) — here 42 without the *p
let s = p + 1; // &i32 + i32 works too → 43
This "automatic on read, manual on write" is what trips you at *v = value later — but that's section 5.
2. && is the address of an address
This is where I actually asked, out loud, "is && the address of an address?" It is. And only after confirming that did "layers" stop being a mental metaphor and become an actual chain in memory.
&&x is the address of "the variable holding x's address." Same as C's double pointer (int**).
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
Peeling follows the chain, one hop at a time.
*pp = 0x...5e4 (&i32) one layer → back to p (x's address)
**pp = 42 (i32) two layers → x's value
So the layer count is "how many times you follow an address before a value appears." &&i32 is two hops (**pp), &i32 is one (*p), i32 is zero. That becomes the ruler in section 3.
3. Counting layers — let a = b does nothing
This is what stumped me the longest. let a = b, let a = *b, let a = &b — I genuinely couldn't tell them apart. Then I printed every type, and the answer came out in one line. Just count the layers.
Here's how the three forms move the layer count.
| written | position | layers |
|---|---|---|
&x | expression | +1 (wrap one) |
*x | expression | −1 (peel one) |
&p | pattern | −1 (peel one on receipt) |
And let a = b does nothing. The right-hand type comes straight through (±0 layers). That was the whole reason the three blurred together. = b is identity, = &b is +1, = *b is −1 — not three different operations, but three points on the same ruler.
Every type, measured (type_name_of_val):
--- starting from b: i32 (0 layers)
let a = b -> i32
let a = &b -> &i32 (+1)
let a = &&b -> &&i32 (+2)
--- starting from r: &i32 (1 layer)
let a = r -> &i32 (±0)
let a = *r -> i32 (−1)
let a = &r -> &&i32 (+1)
The things that don't work fall out of the same arithmetic.
let b: i32 = 7; let a = *b; -> error[E0614]: type `i32` cannot be dereferenced
nothing left to peel at 0 layers
let b: i32 = 7; let &a = b; -> error[E0308]: expected `i32`, found `&_`
left side expects 1 layer, right side has 0
And let *a = b failing is a problem before any layer math — there's simply no * syntax in pattern position.
let b: i32 = 7; let *a = b; -> error: expected pattern, found `*`
4. & in pattern position — a mirror of the expression
This was the trigger. In for &i in indices, I couldn't work out why the & was there. I was reading the i on the left of for as a "variable declaration." So &i looked like it was "declaring a reference," which never made sense. On top of that, I knew "&a is accessed with *" — and the & in a pattern seemed to flatly contradict that.
Both confusions come from the same misread: the left of for or let is not a variable declaration, it's a pattern. A pattern doesn't name things — it matches shapes.
And the reason the & in a pattern is "peel," not "make," is a single one — a pattern is the mirror of an expression. Draw on the left the shape the value would have if it were built on the right, and the compiler runs it backward.
// build (expression) // take apart (pattern)
let e1 = &n; let &p1 = e1; // wrapped with &, so peel with &
let e2 = Some(n); let Some(p2) = e2 else { return };
let e3 = (n, n + 1); let (p3, _) = e3;
So my earlier belief — "&a is accessed with *" — wasn't wrong, it was just only true in expression position. The same peel happens in two worlds, each with its 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 just for i in indices { let i = *i; }. The item from indices.iter() is &usize (1 layer), but get_mut wants usize (0 layers), so a −1 has to happen somewhere. Do it on receipt (pattern) and you get for &i; do it at the point of use (expression) and you get get_mut(*i). Two spellings of the same arithmetic.
5. * on the left of an assignment — rebinding vs. writing
Here's the real reason my original code was broken. I'd written this:
let v = slice.get_mut(i);
v = value; // I believed this would change the slice
I thought v = value would change the element. The slice didn't budge. v is a binding, and putting a new value into a binding moves the name tag somewhere else — it doesn't touch the original.
v = value and *v = value look alike and do the opposite.
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 on read, manual on write" earns its keep here. The compiler will peel for you on a read, but on a write you have to point at the slot yourself with *. So the * on the left of an assignment reads best not as "extract the value" but as "pick the slot to write to." Write v = value without the * and the compiler shrugs — "ah, moving a name tag" — and the slice stays put.
Pulling it together
The fix for my wrong answer was these three lines, and all three were variations on the same &/* story.
for &i in indices { // −1 in a pattern : &usize → usize
if let Some(v) = slice.get_mut(i) { // peel the shell in a pattern : Option → &mut i32
*v = value; // pick the slot in an expression : write to the original
}
}
Once I got that & and * aren't two pieces of syntax but one ruler, the three lines read by a single rule with nothing extra to memorize. Wrap in an expression (+1), peel in either an expression or a pattern (−1), and point at a slot with * only when writing. I already knew C's &/*, so all I actually learned was two things — the mirror that is a pattern, and the habit of counting layers.