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.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.