Arrays should implement `Index` and `IndexMut`

Arrays don't currently implement Index and IndexMut, but slices do, and arrays coerce to slices in a manner I believe similar to as if they actually implemented Deref<Target=[I]>, which they don't. Normally this setup works just fine, but it becomes a problem when you implement Index<MyIndexType> for an array, because then you lose all the other Index implementations that used to be available to the array via its implicit coercion to a slice. An example of what I'm talking about:

use std::ops::Index;

pub enum MyIndexType {
    _0, _1, _2, _3, _4, _5, _6, _7,
}

impl<I> Index<MyIndexType> for [I; 8] {
    type Output = I;

    fn index(&self, index: MyIndexType) -> &I {
        unsafe { self.get_unchecked(index as usize) }
    }
}

fn main() {
    let array = [11u8; 8];

    println!("{:?}", array[MyIndexType::_0]); // OK
    
    println!("{:?}", array[0usize]); // error[E0277]
    //               ^^^^^^^^^^^^^ `[u8; 8]` cannot be indexed by `usize`
}

This problem goes away if we re-implement all the indexing (that is currently provided via the implicit coercion to a slice) directly to all arrays by using const generics. Now that const generics are good enough to be considered for some other traits, perhaps this would be feasible as well. Or would this be a breaking change?

7 Likes

Historically, the "solution" has been to always impl index on the slice type.

This example shows a legitimate reason to impl index directly on an array type, however: types that are known to index into a fixed sized array can use unchecked indexing.

Of note, of course, is that a forwarding implementation of Index[Mut] for [T; N] would not be for some set of indexing types but instead a blanket impl over any SliceIndex<[T]> type. The fact that it's a blanket impl makes adding an impl potentially breaking, but I believe that because SliceIndex is still unstable to implement that it would be a backwards-compatible change (for stable downstream).

(Alternate design that I really don't know about and potentially requires more than const generics MVP: impl<I, T; N: usize> SliceIndex<[T; N]> for I where I: SliceIndex<[T]>; impl<I, T, N> Index<I> for [T; N];. Is this complexity deserved? I honestly don't know yet.)

This would also probably suggest inlining definitions of other indexing methods (anything that takes impl SliceIndex<Self>) such as slice::get as well.

Just for completeness sake, I believe the forwarding implementation would be literally:

use std::{
    array::FixedSizeArray,
    ops::{Index, IndexMut},
    slice::SliceIndex,
};

impl<T, I, const N: usize> Index<I> for [T; N]
where I: SliceIndex<[T]>
{
    type Output = I::Output;

    #[inline]
    fn index(&self, index: I) -> &I::Output {
        &self.as_slice()[index]
    }
}

impl<T, I, const N: usize> IndexMut<I> for [T; N]
where I: SliceIndex<[T]>
{
    #[inline]
    fn index_mut(&mut self, index: I) -> &mut I::Output {
        &mut self.as_mut_slice()[index]
    }
}
2 Likes

What should I do in order to try to get these new implementations added to the standard library? Should I create a pull-request to add them to https://github.com/rust-lang/rust/tree/master/src/libcore/array? If so, then it's just that I don't currently know how to do that. I could try to figure it out on my own but I'd prefer it if someone more knowledgeable did the pull-request instead.

EDIT: I created a pull request here

1 Like