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.