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 of match. Here & strips a reference. And * doesn't exist at all.
  • Reference — the address where some value lives. You make one with &x. &i32 is "the address where an i32 sits."
  • Dereference — following that address to reach the value. You do it with *p. Same as C's *p.
  • Binding — the a in let 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. i32 is 0 layers, &i32 is 1, &&i32 is 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.

Rust
let x: i32 = 42;
let p = &x;    // C's &x: the address of x
TEXT
&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.

Rust
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**).

Rust
let x: i32   = 42;
let p: &i32  = &x;    // p = &x     (address of x)
let pp: &&i32 = &p;   // pp = &p    (address of where p sits)
TEXT
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.

TEXT
*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.

writtenpositionlayers
&xexpression+1 (wrap one)
*xexpression−1 (peel one)
&ppattern−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):

TEXT
--- 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.

TEXT
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.

TEXT
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.

Rust
// 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.

TEXT
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:

Rust
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.

Rust
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.

Rust
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.