Help understanding references to vectors

I have a basic question about understanding references to vectors:

fn main() {
    let v: Vec<i32> = vec![1,2,3,4];

    let third = &v[2];
    // The book says that third gives a reference to the element.
    // This is correct. assert_eq!(third, 3) fails but assert_eq!(third, &3)
    // succeeds.
    assert_eq!(third, &3);

    let r = &v;
    // r is a reference to v
    // what is r[2]? Or, rather why is it 3? 
    // Either indexing a reference should make no sense.,
    // or should r[2] be not the same as &v[2]?
    assert_eq!(r[2], 3); // this succeeds, why?
}

From the reference (plus some emphasis):

Array and slice-typed values can be indexed by writing a square-bracket-enclosed expression of type usize (the index) after them. When the array is mutable, the resulting memory location can be assigned to.

For other types an index expression a[b] is equivalent to *std::ops::Index::index(&a, b), or *std::ops::IndexMut::index_mut(&mut a, b) in a mutable place expression context. Just as with methods, Rust will also insert dereference operations on a repeatedly to find an implementation.

Based on the fact that Vec<T> implements the Index<…> trait, but references don’t, this means that r[2] will desugar to *Index::index(&*r) with the extra dereference * in front of the r that makes &*r be of type &Vec<i32>, not &&Vec<i32> and thus match the &self argument type of the implementation Index<usize> for Vec<i32>.

The Index::index call then returns a &i32 reference to the element, and the desugaring makes sure to dereference this result (see the “*” at the very beginning) so that r[2] is of type i32, and you’d need to write &r[2] if you wanted a &i32. Feel free to ask follow-up questions if any of this explanation is confusing or otherwise not clear to you.

By the way, the remark “just as with methods” hints at how methods have a similar feature (even a bit more powerful, e.g. also allowing references to be added) as documented here.

1 Like

Thank you so much for such a clear explanation.

Explained in a different way, it has to do with operator precedence. The reference tells us that indexing has a higher precedence than taking a reference. Therefore, &v[2] is equivalent to &(v[2]). In the second example, let r = &v; r[2] does the referencing first, so it's not equivalent to &v[2].

2 Likes

Oh, I didn’t even catch that potential source of confusion, but with that pointed out, the comment

should r[2] be not the same as &v[2]

makes some more sense.

Indeed, the v[2] binds stronger, desugars to *Index::index(&v, 2), and then, as a consequence, the & added to the front makes &v[2] sugar for &*Index::index(&v, 2). The return type of this Index::index call is &i32; the dereference that comes from the desugaring means we get a i32 as the type of v[2] / *Index::index(&v, 2), but these are place expressions, so the actual place of the i32 in the vector in memory is not yet forgotten by the dereferencing alone, so that adding the & in front gives us a &i32 (pointing to the same place) again, as the type for &v[2] of &*Index::index(&v, 2).

(Other so-called “place expression contexts” where the fact that v[2] refers to a place, not just a value, become relevant include e.g. assignment operators [though those also go through IndexMut, not Index], so that v[2] = 42 or v[2] += 1 work as expected.)

2 Likes

Thank you both. I was thinking of whether I could also make sense of this in terms of operator precedence and was going to ask today. So, thanks for clarifying that.