Index out of bounds? Not always! - A Rusty Surprise
Serhii Potapov January 04, 2024 #rust #array #index #slice #Deref CoercionI recently encountered a curious situation with Rust's array indexing that caught me off guard. Rust, known for its vigilant error checking, especially with out-of-bounds array access, presented an interesting case.
Rust's Protective Error Checks on Array Indices
Let’s look at a very basic example:
let arr: = ;
let third = arr;
Rust, like a watchful guardian, throws an error:
|
2 | let third = arr[2];
| ^^^^^^ index out of bounds: the length is 2 but the index is 2
It’s straightforward: an array with two elements doesn’t have a third slot. Rust, with its compile-time checks, easily spots such errors. So far so good.
A Twist in My Code
In my coding journey, I wrote something like this:
I assumed that Rust would alert me if the size of steps
changed or if I accidentally accessed a wrong index. To test assumption, I altered first_step_id
and
replaced 0
with 5
:
To my surprise, this code compiled without any errors.
Unraveling the Mystery
The clue to this puzzle lies in whether step_group
is passed by value or reference. Switching to pass-by-value makes Rust vigilant again:
And there’s the expected error:
|
10 | step_group.steps[2].id
| ^^^^^^^^^^^^^^ index out of bounds: the length is 2 but the index is 2
I suggest taking a break from reading now and, as an exercise, try to find the explanation on your own.
Then we can compare our results :)
A Closer Look
Here’s my take on what’s happening, based on my understanding of Rust.
Rust has a trait called std::ops::Index
for indexing operations. When we use an index like arr[2]
, we're actually invoking the index
function of this trait.
The Index
trait is generically implemented for arrays, as seen in the documentation:
This kicks in when step_group
is passed by value.
However, when step_group
is a reference, like &StepGroup
, the situation changes.
The expression step_group.steps[2]
apparently becomes a call to Index::index
for &[Step; 2]
.
Now, we’re dealing with a reference, not the array itself.
But here is no implementation of Index
for &[T; N]
! However, we can see that there is Index
implemented for [T]
(a slice),
and we know that a &[T; N]
can be dereferenced to a slice.
So Rust applies Deref coercion here.
Eventually accessing &[Step; 2]
turns the reference to an array into a slice, and the slice’s Index
implementation is used.
And a slice has no notion of a size at compile time.
Shrinking the example
Here's a minimal example that illustrates this quirky behavior. This code compiles, though it clearly accesses an index that doesn’t exist:
let arr: = ;
;
Conclusion
Though I have written the article, and gave the explanation to the odd behavior, I am seeking for external confirmation to see if I am right.
I also question myself, would the compiler gave a proper error message, if there was an implementation of Index
trait for &[T; N]
?
Update
By now I got some feedback from the Rust community and I think this comment from Sharlinator explains the mystery:
Note that neither array nor slice indexing with
[]
actually goes through theIndex
trait at all. The operators are intrinsic and hardcoded into the compiler, just like all other operators of the builtin types. The trait impls only exist to facilitate generic use.
Thanks!