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, a match arm. 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. &i32 means "the address of an i32".
  • dereference — following that address to read the value, written *p. Same as C's *p.
  • binding — the a in let 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. i32 is depth 0, &i32 is depth 1, &&i32 is 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.

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

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

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

Dereferencing follows the addresses one level at a time.

TEXT
*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 writepositiondepth
&xexpression+1 (creates one level of reference)
*xexpression−1 (follows one level of reference)
&ppattern−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):

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

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

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

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

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

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

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

Rust
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 = b doesn'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.