To start with the obvious, this is not possible in the general case. E.g., where the Key
type is a superset of the collection's key set. For instance, if Key
is a usize
, then on 64-bit platforms there are 264 possible keys to index the collection. Most collections (sans ZSTs) are not going to have capacity available to ensure that indexing with usize::MAX
does not panic.
There are cases where the Key
type is guaranteed to be an equivalent set (or even a subset) of the collection's keys (perfect hash, u8
keys for collections with 256 values, etc). And I suspect this is what you are getting at. Then the question becomes "how do you guarantee that an Index
impl does not panic?"
To generalize further, we would need to know how any function can be guaranteed non-panicking. While the language doesn't support a notion of "guaranteed infallibility", there are some tricks that can be used to achieve the desired result. For instance, the no-panic
crate provides an attribute macro that forces the compiler to prove an annotated function does not panic. It isn't perfect (see the caveats listed in docs) but it's what is currently available.
Knowing this, let's write a test!
use no_panic::no_panic;
use std::ops::Index;
struct MyStruct {
data: [u32; 256],
}
impl Default for MyStruct {
fn default() -> Self {
Self { data: [0; 256] }
}
}
impl Index<u8> for MyStruct {
type Output = u32;
#[no_panic]
fn index(&self, idx: u8) -> &Self::Output {
&self.data[idx as usize]
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::hint::black_box;
#[test]
fn test_index() {
let mine = MyStruct::default();
for i in 0_u8..=255 {
println!("Index {i} = {}", mine[i]);
assert_eq!(black_box(mine[i]), black_box(0));
}
}
}
When I run this with cargo test
, I get a compile error due to the panicking path in the debug build (see aforementioned caveats). On the other hand, cargo run --release -- --nocapture
prints all 256 lines and the test passes.
I suppose one may conclude that the Index
trait is actually what you want, iff you can do something like this example and ensure that the impl does not panic. Unfortunately, there doesn't seem to be a way to do that without hacks like this #[no_panic]
annotation. (Although it allows any function to be annotated, including the top-most functions in your application...)
Edit: there is also the TryFrom
trait, which can do something similar but provides Result
instead of guaranteeing infallible indexing. It is unusual to use it this way, though.
#[derive(Copy, Clone)]
struct WrappedU32<'a>(&'a u32);
impl<'a> TryFrom<(&'a MyStruct, usize)> for WrappedU32<'a> {
type Error = ();
#[no_panic] // Annotated to prove it doesn't panic
fn try_from(value: (&'a MyStruct, usize)) -> Result<Self, Self::Error> {
match value.1 {
0..=255 => Ok(WrappedU32(&value.0.data[value.1])),
_ => Err(()),
}
}
}
impl WrappedU32<'_> {
pub fn to_u32(self) -> u32 {
*self.0
}
}
#[test]
fn test_try_from() {
let mine = MyStruct::default();
// Iterate more than 256 indices to check fallibility
for i in 0_usize..=512 {
match WrappedU32::try_from((&mine, i)) {
Ok(value) => {
let value = value.to_u32();
println!("Index {i} = {value}");
assert_eq!(black_box(value), black_box(0));
}
_ => println!("Index {i} = ERROR"),
}
}
}