Why doesn't `[T; N]` implement `std::ops::Index`?


#1

Why don’t the array types implement std::ops::Index? I would expect this to work:

use std::ops::Index;

fn first<T: ?Sized>(a: &T) -> T::Output
    where T : Index<usize>,
          T::Output : Copy
{ a[0] }

fn main() {
    let ten = first(&[10,20,30]);
}

But I get an error:

error: the trait bound `[_; 3]: std::ops::Index<usize>` is not satisfied [--explain E0277]
 --> <anon>:9:15
9 |>     let ten = first(&[10,20,30]);
  |>               ^^^^^
note: the type `[_; 3]` cannot be indexed by `usize`
note: required by `first`

error: aborting due to previous error

It seems that not being able to use Index as a bound for, well, indexable things is a missed opportunity.


#2

Note that if I change the call to first to read:

    let ten = first(&[10,20,30] as &[i32]);

everything works fine.


#3

I am not sure, but I wonder if this is because arrays only implement it up to length 32, but the type system can’t understand that.


#4

I think generally when an array would behave exactly the same as a slice, then it’s just left to the slice implementation through deref coercion. For some reason that doesn’t work here, but another option in this case is to convert it with as_ref(). You can see the deref coercion with my second here:

use std::ops::Index;

fn first<T: ?Sized>(a: &T) -> T::Output
    where T : Index<usize>,
          T::Output : Copy
{ a[0] }

fn second<T : Copy>(a: &[T]) -> T
{ a[1] }

fn main() {
    let ten = first([10,20,30].as_ref());
    let twenty = second(&[10,20,30]);
}

#5

Another option:

fn third<T: ?Sized, U: ?Sized>(a: &T) -> U::Output
    where T : AsRef<U>,
          U : Index<usize>,
          U::Output : Copy
{ a.as_ref()[2] }

    let thirty = third(&[10,20,30]);

#7

Okay, not literally a std::ops::Deref coercion, because arrays don’t implement that, but a coercion as described in RFC401. I guess my question reduces to, why isn’t that coercion occurring here? Is that a bug?

According to the MIR, the RFC401 conversion of &[i32; 4] to &[i32] does take place in the below code, and then we invoke <[i32] as std::ops::Index<usize>>::index on the slice:

use std::ops::Index;

fn main() {
    let a : [i32; 4] = [1,2,3,4];
    let one = *a.index(0);
}

#8

My wild guess is that T is getting locked to the exact array type, and only then checked whether it fulfills the requirements. I guess to do what you want, it would have to try every T that arrays can be coerced into (just a slice?) until it found the right fit.

That’s pretty much what happens with method invocation though, as your a.index(0) shows, so I’m not sure why it couldn’t be done for the generic T too.


#9

TL;DR: Unless the array cannot be converted to the type needed (ignoring where) the array will be an array. This is expected behavior as defined in RFC 0401.

Well T is the type of whatever is passed, you are then placing constraints on what should be passed, i.e. anything implementing Index. Unfortunately your T is [i32; 4] which does not implement Index as of the documentation. Arrays do coerce into slices, but only if the required type is a slice, your function’s argument is not a slice so it isn’t coerced and therefore stays an [i32; 4] which does not implement Index and is not allowed to be passed to the function.

This is stated in the mentioned RFC:

Note that we do not perform coercions when matching traits (except for receivers, see below). If there is an impl for some type U, and T coerces to U, that does not constitute an implementation for T. For example, the following will not type check, even though it is OK to coerce t to &T and there is an impl for &T:

struct T;
trait Trait {}

fn foo<X: Trait>(t: X) {}

impl<'a> Trait for &'a T {}


fn main() {
   let t: &mut T = &mut T;
   foo(t); //~ ERROR failed to find an implementation of trait Trait for &mut T
}

In a cast expression, e as U, the compiler will first attempt to coerce e to U, and only if that fails will the conversion rules for casts (see below) be applied.


#10

Okay, that is a very clear and well-sourced description of the behavior I’m seeing. I think it also gives me some insight into how type variable bounds on generic functions are processed. Thanks!

It seems to me that arrays should implement Index and IndexMut. The conversions that would otherwise bring appropriate implementations into play don’t apply in places where they would be valuable.


#11

I myself don’t really like to work with arrays, I prefer using slices as they do have a little more functionality and are essentially the same, just without the hassle of having distinct types for [T;1] and [T;2]. If I use them I only use them as a backend-storage and pass them further using slices.
Is there any significant disadvantage to this?


#12

I also think slices are easier to work with. But there may be a slight disadvantage that slices are fat (pointer and length) whereas an array reference is just a pointer. And it may be easier for the optimizer when the length is encoded into the type, rather than checking a slice’s runtime length or hoping for deep constant propagation. I suspect it’s not generally significant though.