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

While learning Rust I worked through a simple exercise that modifies slice elements. The compiler gave me errors one at a time, I fixed each one as it suggested, and I ended up with code that compiles but changes nothing. Working out why led me to the difference between & and &mut.

This post covers what a slice actually is, why iter() and iter_mut() are separate, and why the * in *item is required. It does not cover split_at_mut and unsafe, reborrowing, or comparisons with index loops. Every error message, address and size below comes from running the code on rustc 1.96.0.

This is the code I started with.

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. In Rust every value has 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 a reference borrowed for modifying. A borrow has to be returned eventually, so a reference can't outlive the original.

The Copy trait. A marker on types where moving a value can leave the original in place and make a copy instead. Small types that don't own resources outside themselves, like i32, are Copy. String owns data on the heap, so it isn't. This distinction matters later.

Unsized types and fat pointers. A type whose size isn't known at compile time is called unsized. [i32] is one — the type doesn't say how many i32s there are. Such a type can't be stored in a variable directly and is always handled through a reference. That reference needs the length as well as the address, and a two-field pointer like this is called a fat pointer.

IntoIterator and auto-deref. for x in y is syntactic sugar: it calls y.into_iter() and loops over the resulting iterator. Auto-deref is the compiler inserting a * for you on method calls; item.abs() becomes (*item).abs().

1. A slice is a reference to part of someone else's data

[i32] and &[i32] look alike but are different types. Try to store the first in a variable and you get an error.

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 "i32s laid out contiguously, number unknown." With no known size it can't get a slot on the stack, so it's always handled through a reference. Measuring the sizes shows the difference.

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 an 8-byte length is added to the 8-byte address. An array like [i32; 3], whose count is part of the type, is 12 bytes and is the data itself.

This defines what a slice is. A slice doesn't own data. It refers to part of data stored elsewhere and holds only "where it starts and how many."

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

So slice: &mut [i32] in the signature means the function receives "one slice borrowed so it can be modified." It doesn't own it, so there's nothing to return. That's why the original code's attempt to return slice at the end was wrong.

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

It's a function that modifies in place, so there's nothing to return.

2. The address is the same; the permission differs

I thought an address only came through once you used iter_mut(). My question was, "So with iter() I can't modify item, and I have to use iter_mut() to get an address I can modify through?" That was half right.

iter() gives you an address too. Printing them shows it.

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)

The addresses are identical. The sizes match too: &i32 and &mut i32 are both 8 bytes. To the machine, they're indistinguishable pointers.

So the difference isn't whether you get an address but what permission comes with it.

  • &i32 — you can only read through this address
  • &mut i32 — you can read and write through this address

The permission exists only in the type. &i32 and &mut i32 are just different types, and when the compiler sees *x = ... it only checks whether x is a &mut _. After the check, nothing about it remains in the binary. There's no runtime cost.

Try to write through a read-only address and you get this.

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 have the address, and dereferencing works. Only writing is blocked, because there's no write permission.

3. &mut means exclusive access

"Interesting, but how come the mut one can be changed and the non-mut one can't?" — I wanted to know why permission is split at all.

The core of &mut is exclusive access rather than write permission. Compiling three cases shows the rule.

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)

Any number of read references can exist at the same time. Only one write reference can exist, and while it does, no read reference can exist alongside it. Either many places read at once, or one place modifies.

Why the second case is dangerous shows up when you keep pushing to a Vec. When capacity runs out, all the data is reallocated to a different place on the heap.

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

("move" in this output means reallocation, not a Rust move.)

If you'd taken a reference with &v[0] before a reallocation, afterwards that reference points to memory that is no longer valid. In C++ this bug is called iterator invalidation, and Rust prevents it at compile time with E0502. While a reading reference exists, nothing can change the structure.

One thing to note: that reallocation doesn't always happen. realloc grows in place when the space after it is free, and in fact pushing three times into Vec::with_capacity(2) left the address unchanged. Only after 20000 pushes did I see ten reallocations.

So reallocation is something that can happen. The compiler rejects the code on that possibility alone. Rather than picking out the cases that actually go wrong, it rejects all code that could. That's why Rust sometimes feels overly strict.

Now the function signature reads differently. Receiving slice: &mut [i32] means receiving a guarantee that "this function is the only thing accessing this slice right now." That guarantee is why modifying it is allowed: nothing else is reading it at the same time, so modifying it causes no problems.

4. What for actually calls, and the three methods

I was curious about iter_mut(). With iter() already there, was it only for mutable slices, or something else?

It isn't only for mutable slices. Which one you can call is determined not by the slice's type but by the self the method requires.

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

So the rule isn't symmetric. iter() can be called anywhere; iter_mut() only when you have mutable access.

what you haveiter()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 to notice is iter() passing on &mut [i32]. Whoever holds a mutable reference is free to use it as a shared one. Permission can be lowered from stronger to weaker, but not the other way.

It's also worth noting that the same E0596 comes with different wording. For &[i32], "behind a & reference" points out that the borrowed path is immutable; for let arr, "not declared as mutable" points out that the owned variable is immutable. mut is the access permission you hold at that moment.

for is syntactic sugar on top of this. for x in y calls y.into_iter(), and what into_iter() returns depends on what it's called on.

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 &T and you get &T; hold &mut T and you get &mut T. iter() and iter_mut() are methods that choose two of those cases explicitly.

5. & and * look alike and do opposite things

Back to the code at the top. It was code the compiler couldn't catch. Clear the errors one by one and it ends up compiling.

Remove the return and the second error appears.

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 suggests adding mut. Do that and it compiles — and nothing happens. A warning from the same compile pointed to the real cause.

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

slice.iter() yields &i32, and the pattern &item destructures it and copies out an i32. item is a copy, not the original. Printing the addresses makes it clear.

TEXT
  iter()      item        = 0xf350ddf49c   // points at the original
  &item pattern  copy at  = 0xf350ddf5ac   // a different location, the same one on all three iterations

The copy reuses the same stack location on every iteration. It has nothing to do with the original.

This shows & working in opposite directions in two positions. In type position & creates a reference; in pattern position & removes one. So for &item in is syntax for unwrapping the reference and taking out the value.

Side by side, the three cases look like this.

what you writewhat you getmodify the originalcompiler response
for item in slice.iter()a read-only addressnoblocked with E0594
for &item in slice.iter()a copy of the valuenocompiles without errors
for item in slice.iter_mut()a read-write addressyescompiles

If I'd picked the first line, the compiler would even have told me use mutable method: iter_mut(). Adding the & changed the problem from "missing permission" to "modified a copy and threw it away," and the error turned into a warning.

That's the nature of this bug. The &item pattern turns the reference into a value, so there's no longer anything for the borrow checker to check. What's left is an ordinary logic bug — modifying a local variable and discarding it — and the borrow checker isn't a tool for catching logic bugs.

And this conversion happens without an error because i32 is Copy. If the elements were String, the same pattern would be rejected like this.

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

The problem only happens with Copy types. With String, the compiler would have blocked it from the start.

6. A * on the left of an assignment specifies where to write

"Ah, so since iter() or iter_mut() gives me an address, I need to write *item to change the actual value?" — that was my next thought. It's right, but checking it showed that writing and reading differ.

When writing, there are 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

When reading, you can sometimes leave out the *, depending on whether it's 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 pass in both cases, because auto-deref turns them into (*item).abs(). Operators, on the other hand, pass only for &i32: the standard library implements &i32 % i32 but has no version for &mut i32. That's why the * in if *item % 2 != 0 in the fixed code is actually needed.

Writing has no exceptions because item is itself a variable — one holding an address. Writing item = ... means "put a different address in this variable," which changes the address being pointed to, not the value at it. item -= 1 is E0368 because it would mean subtracting 1 from an address.

Only *item -= 1 means "subtract 1 from the value at the location this address points to." So a * on the left of an assignment is more accurately read as an operator that specifies the location a reference points to.

The & in for &item in and the * in *item both look like they remove a reference, but one reaches a copy and the other reaches the original.

7. An aside: % is not modulo

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

It isn't a crack; it's the idiom, and == 1 is the wrong 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 for negatives. There's also an is_multiple_of method, but it exists on u32 and not on i32.

Summary

The fixed code looks like this.

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 things changed: iter() became iter_mut(), the pattern &item became plain item, and the assignment dereferences with *. The return statement was removed.

  • &i32 and &mut i32 hold the same address; what differs is the permission.
  • &mut means exclusive access, so you can't create one while a reading reference exists.
  • Even with a &mut [i32], calling iter() gives you read-only elements. To modify them, pass the mutable permission on with iter_mut().
  • The pattern &item removes the reference and copies the value. For a Copy type like i32 this compiles without errors, and you end up modifying only a copy.
  • The original changes only when you specify where to write with *, as in *item.