Same Address, Different Permission: Why My Rust Slice Wouldn't Change

(edited: July 10, 2026)

I was learning Rust and hit a simple exercise: modify the elements of a slice in place. The compiler threw errors one at a time, I fixed each one exactly as it asked, and ended up with code that compiles and does nothing at all. Chasing that down took me all the way to the difference between & and &mut.

This post covers what a slice actually is, why iter() and iter_mut() diverge, and why the * in *item is not optional. It does not cover split_at_mut and unsafe, reborrowing, or how index loops compare. Every error message, address, and size below came from running the code on rustc 1.96.0.

Here is where it started.

Rust
pub fn transform_even_odd(slice: &mut [i32]) {
    // Your code here: iterate over the mutable slice and modify its elements.

    for &item in slice.iter() {
        if item % 2 == 1 {
            item -= 1
        }
    }

    slice
}

Before we start — some terms

Borrow and reference. Every value in Rust has exactly one owner. The owner can lend the value out temporarily, and what the borrower holds is a reference. &T is a reference borrowed for reading; &mut T is one borrowed with permission to change. Anything borrowed must eventually be returned, so a reference can never outlive the thing it points at.

The Copy trait. A marker for types that can be duplicated bit-for-bit instead of moved. i32 qualifies: it is small and holds no resources outside itself. String does not, because it owns data on the heap. This distinction turns out to be the heart of this post.

Unsized types and fat pointers. A type whose size is unknown at compile time is unsized. [i32] is one — nothing in the type says how many i32s there are. Such a type cannot live in a variable; it only ever exists behind a reference. That reference needs more than an address, so it carries a length alongside it. A two-word pointer like this is called a fat pointer.

IntoIterator and auto-deref. for x in y is sugar: it calls y.into_iter() and drives the resulting iterator. Auto-deref is the compiler quietly inserting * for you at method calls, so item.abs() becomes (*item).abs() without you writing it.

1. A slice is not data. It is a borrowed window

[i32] and &[i32] look alike but are different types. Try to put the first one in a variable and you get turned away.

TEXT
error[E0277]: the size for values of type `[i32]` cannot be known at compilation time
  |         ^^ doesn't have a size known at compile-time
  = help: the trait `Sized` is not implemented for `[i32]`

[i32] means "some i32s laid out back to back, count unknown." With no size, the compiler cannot reserve a slot for it, so it only ever exists behind a reference. Measuring makes the split obvious.

TEXT
size_of::<&i32>()       = 8    (thin pointer: address only)
size_of::<&[i32]>()     = 16   (fat pointer: address + length)
size_of::<&mut [i32]>() = 16
size_of::<[i32; 3]>()   = 12   (an array knows its size)

&[i32] is 16 bytes because it stacks an 8-byte length on top of an 8-byte address. An array like [i32; 3], whose count is baked into the type, is 12 bytes — it is the data.

That difference defines what a slice is. A slice owns nothing. It is a window laid over someone else's data, carrying only "start here, this many."

TEXT
original starts at 0x4664aff5a8,  slice starts at 0x4664aff5ac, length 2   // &v[1..3]

So slice: &mut [i32] in a signature means "one window, borrowed with permission to change." Nothing is owned, so nothing needs handing back. That is why the code at the top was wrong to return slice.

TEXT
error[E0308]: mismatched types
10 |     slice
   |     ^^^^^ expected `()`, found `&mut [i32]`

It edits in place. There is nothing to return.

2. The address is the same. What differs is permission

I assumed iter_mut() was the one that handed me an address — that iter() gave me values, and only iter_mut() gave me something I could write through. I was half right.

iter() hands you an address too. Print them and it is plain.

TEXT
array starts at 0xf350ddf49c
  iter()     item = 0xf350ddf49c  (value 10)
  iter()     item = 0xf350ddf4a0  (value 20)
  iter()     item = 0xf350ddf4a4  (value 30)
  iter_mut() item = 0xf350ddf49c  (value 10)
  iter_mut() item = 0xf350ddf4a0  (value 20)
  iter_mut() item = 0xf350ddf4a4  (value 30)

Identical addresses. Identical sizes too: &i32 is 8 bytes and so is &mut i32. To the machine they are indistinguishable pointers.

So the difference is not whether you get an address. It is what permission is attached to it.

  • &i32 — read through this address, nothing more
  • &mut i32 — read and write through this address

The permission exists only in the type. &i32 and &mut i32 are simply different types, and when the compiler sees *x = ... it checks whether x has type &mut _. Once that check passes, the information leaves no trace in the binary. It costs nothing at runtime.

Write through a read-only address and you are stopped here.

TEXT
error[E0594]: cannot assign to `*item`, which is behind a `&` reference
2 |     for item in slice.iter() {
  |                 |     help: use mutable method: `iter_mut()`
  |                 this iterator yields `&` references
3 |         *item -= 1;
  |         ^^^^^^^^^^ `item` is a `&` reference, so it cannot be written to

You hold the address. You can dereference it. Only writing is refused. Not for lack of an address — for lack of permission.

3. &mut does not mean "writable." It means "nobody else is looking"

But why split permissions at all? Why does the one marked mut get to change things and the other one doesn't? That struck me as oddly arbitrary.

The meaning of &mut is not write access. It is exclusivity. Compile three cases and the rule shows itself.

TEXT
// two &mut at once
error[E0499]: cannot borrow `*v` as mutable more than once at a time

// a & is alive and you take a &mut anyway
error[E0502]: cannot borrow `*v` as mutable because it is also borrowed as immutable

// two & at once
(passes)

Read references coexist freely. There is exactly one write reference, and while it lives, not even a read reference may sit beside it. Share it or change it, never both.

Why the second case is dangerous becomes visible when a Vec grows. Once capacity runs out, the data moves house.

TEXT
move #1: at len      5,  0x1c18595a9f0 -> 0x1c185956460
move #2: at len      9,  0x1c185956460 -> 0x1c1859529c0
move #3: at len     17,  0x1c1859529c0 -> 0x1c185954480

across 20000 pushes, the data moved 10 times

If you took a reference with &v[0] before the move, that reference now points at an empty house nobody lives in. C++ calls this iterator invalidation. Rust stops it at compile time with E0502: while a reader exists, nobody may restructure.

One honest caveat. The move above does not always happen. realloc will grow in place when the space behind it is free, and in fact pushing three times into a Vec::with_capacity(2) left the address untouched. It took twenty thousand pushes to catch ten moves.

The address does not always change. It may change. And the compiler refuses on the "may" alone. It does not try to isolate the genuinely dangerous moments; it rejects every program where danger is possible. That is exactly where Rust starts to feel obstinate.

Now the signature reads differently. Receiving slice: &mut [i32] means you were handed a guarantee: right now, you are the only one looking at this slice. Permission to change follows from that. Nobody else can be surprised by your edit.

4. What for really calls, and the three siblings

iter_mut() was the piece I kept turning over. There is already iter() — so is iter_mut() something you only use on a mut slice, or is it a different thing altogether?

It is not slice-specific. What separates them is not the type of the slice but the self the method demands.

Rust
pub fn iter(&self)         -> Iter<'_, T>      // a shared reference suffices
pub fn iter_mut(&mut self) -> IterMut<'_, T>   // a mutable reference is required

Which makes the rule asymmetric. iter() can be called anywhere; iter_mut() only when you hold mutable access.

What you holditer()iter_mut()
&[i32]passeserror[E0596]: cannot borrow *slice as mutable, as it is behind a & reference
&mut [i32]passespasses
let arr = [1,2,3]passeserror[E0596]: cannot borrow arr as mutable, as it is not declared as mutable

The cell worth staring at is &mut [i32] calling iter(). Holding exclusive access, you are free to demote yourself to a shared reference. Permission flows down, never up.

The two E0596s even word themselves differently. For &[i32] it says "behind a & reference" — the borrowed path is immutable. For let arr it says "not declared as mutable" — the owned variable is. mut is not a label stuck to a type; it is the access you happen to hold at that moment.

for sits on top of all this as sugar. for x in y calls y.into_iter(). And into_iter() mirrors whatever it receives.

TEXT
from a slice s: &mut [i32]
  s.iter()      -> &i32
  s.iter_mut()  -> &mut i32
  s.into_iter() -> &mut i32        // on a slice, same as iter_mut()
  &mut *s       -> &mut i32        // this works too

from something you own
  [String; 1] .into_iter()  -> String
  Vec<String> .into_iter()  -> String
  &[i32]      .into_iter()  -> &i32

Own a T and you get T. Hold a &T and you get &T. Hold a &mut T and you get &mut T. iter() and iter_mut() are just shortcuts that name two of those cases outright.

5. & and * look alike and do the opposite

Back to the code at the top. It was not merely wrong — it was wrong in a way the compiler does not catch. Erase the errors in order and it slips through in silence.

Delete the return statement and the second error surfaces.

TEXT
error[E0384]: cannot assign twice to immutable variable `item`
2 |     for &item in slice.iter() {
  |          ---- first assignment to `item`
4 |             item -= 1
  |             ^^^^^^^^^ cannot assign twice to immutable variable
help: consider making this binding mutable

The compiler says add mut. Do as told and it compiles. And nothing happens. A warning from the very same run was pointing at the real cause all along.

TEXT
warning: value assigned to `item` is never read

slice.iter() yields &i32, and the pattern &item destructures it, copying out an i32. item is not the original; it is a copy. The addresses say so.

TEXT
  iter()      item        = 0xf350ddf49c   // points at the original
  &item pattern  copy at  = 0xf350ddf5ac   // elsewhere. same slot all three laps

The copy reuses one stack slot every iteration. It has nothing to do with the original.

Here is where & reveals that it does opposite things in two positions. In type position & builds a reference. In pattern position & strips one. So for &item in is not syntax for receiving a reference; it is syntax for peeling one open and taking what is inside.

Line the three variants up.

What you wroteWhat you holdModifies originalCompiler's response
for item in slice.iter()read-only addressnostops you with E0594
for &item in slice.iter()a copy of the valuenopasses silently
for item in slice.iter_mut()read-write addressyespasses

Pick the first and the compiler even tells you use mutable method: iter_mut(). Add the & and the problem mutates from "insufficient permission" into "edited a copy, then dropped it" — and the error is demoted to a warning.

That is the character of this bug. The &item pattern does not defeat the safety net; it changes the problem out from under the net. Peel the reference into a value and the ownership question evaporates, leaving an ordinary logic bug about a local variable. The borrow checker was never a tool for logic bugs.

And this conversion happens quietly only because i32 is Copy. Had the elements been String, the same pattern would be stopped cold.

TEXT
error[E0507]: cannot move out of a shared reference
7 |     for &item in v.iter() { ... }
  |          ----    ^^^^^^^^
  |          data moved here because `item` has type `String`,
  |          which does not implement the `Copy` trait
help: consider removing the borrow

A trapdoor that only opens under Copy types. With String the compiler would have blocked me at the door, and there would have been no post to write.

6. * does not extract a value. It names a place

So if an address comes through, I need *item to touch the real value. That much held up — and then it turned out writing and reading do not follow the same rule.

Writing admits no exceptions.

TEXT
error[E0368]: binary assignment operation `-=` cannot be applied to type `&mut i32`
3 |         item -= 1;
help: `-=` can be used on `i32` if you dereference the left-hand side

Reading gets some leniency, and how much depends on whether you came from iter() or iter_mut().

Expression&i32 (iter())&mut i32 (iter_mut())
*item % 2passespasses
item % 2passesE0369
*item > 0passespasses
item > &0passesE0308
item.abs()passespasses
item.pow(2)passespasses

Method calls are forgiven on both, because auto-deref rewrites them to (*item).abs(). Operators are forgiven only on &i32, because the standard library implements &i32 % i32 and offers no &mut i32 counterpart. Which is why the * in if *item % 2 != 0 was genuinely load-bearing in the fixed code.

Writing has no exception because item is itself a variable — a variable holding an address. Write item = ... and you are saying "put a different address in this variable," redirecting where it points rather than changing what it points at. item -= 1 draws E0368 for the same reason: it reads as subtracting one from an address.

Only *item -= 1 says "subtract one from the slot this address names." So * is less an operator that extracts a value than one that names the place the reference points to, rather than the reference itself. On the left of an assignment it is not fetching anything; it is choosing where to write.

The & in for &item in and the * in *item both wear the costume of stripping a reference. One reaches a copy. The other reaches the original.

7. An aside: % is not modulo

Spotting odd numbers with -3 % 2 != 0 felt like a crack to me — a workaround.

It is not a crack. It is the idiom, and == 1 is the broken one. Rust's % is remainder, not modulo, so the sign follows the dividend.

TEXT
n=-3  n%2!=0 -> true   n&1==1 -> true   rem_euclid -> true
n=-2  n%2!=0 -> false  n&1==1 -> false  rem_euclid -> false
n= 3  n%2!=0 -> true   n&1==1 -> true   rem_euclid -> true

-3 % 2 is -1, not 1. So item % 2 == 1 misses every negative odd number. n % 2 != 0, n & 1 == 1, and n.rem_euclid(2) == 1 all behave correctly on negatives. There is also an is_multiple_of method, available on u32 but not on i32.

Pulling it together

Here is the fixed version.

Rust
pub fn transform_even_odd(slice: &mut [i32]) {
    for item in slice.iter_mut() {   // item: &mut i32
        if *item % 2 != 0 {
            *item -= 1;
        }
    }
}

Three changes. iter() became iter_mut(), the pattern &item became plain item, and the assignment dereferences with *. The return statement is gone.

My misconception turned out to be a single one. I thought that receiving &mut [i32] made its elements automatically editable. But mut is not a label on the data — it is the access I happen to hold at that moment — and when iterating I have to hand that access to the iterator explicitly. Calling iter() was me demoting myself without noticing.

Once I understood that &mut means "you are the only one looking at this" rather than "you may write," the compiler's strictness stopped feeling arbitrary. Being able to write is a consequence of that exclusivity, not the point of it.

What unsettled me most is the place where the compiler stayed quiet. I had assumed Rust catches everything, and then a single & in a pattern turned an ownership problem into an ordinary logic bug and walked it right out of the checker's range. The door only opened because i32 is Copy; with String it would never have compiled. Knowing what a language protects, it turns out, means knowing what it doesn't.