FYI, to talk of actual implementation, and the number of indirections to the data, dereferencing a Vec does actually remove a layer of indirection. The Deref::deref method converts &Vec<T> to &[T]. The former is a reference to a vector of T, and a vector itself consists of an owning pointer to T, as well a length and capacity information. The number of indirections until you reach the Ts in &Vec<T> is 2, and in &[T] it’s 1.
Yet, when reasoning about dereferencing, it’s actually common to talk about the target types, not necessarily the references. I.e. what I mean is, you can think of “dereferencing” Vec<T> to [T] instead of the actual &Vec<T> to &[T] conversion. This approach then allows to generalize over dereferencing for shared-reference access, mutable-reference access, and owned access (the last one only works with Box though, and with Copy types). The first two are then covered by Deref and DerefMut traits. “Dereferencing Vec<T> to [T]” conceptionally thus includes both
- the conversion
&Vec<T> to &[T], and
- the conversion
&mut Vec<T> to &mut [T]
Note that method resolution works on that level, too. It will first reason about such a dereferencing step as going from Vec<T> to [T], and only at a later stage, the actual implementation in terms of either deref or deref_mut, or primitive / compiler-implemented implementations of dereferencing (for reference types and/or for Box) are chosen.
So the concrete example v.chunks(2) with v: Vec<i32> works as follows.
First prepare the list of types to consider. This list is
1a. Vec<i32>
1b. &Vec<i32>
1c. &mut Vec<i32>
2a. [i32]
2b. &[i32]
2c. &mut [i32]
where each step na to (n+1)a is done via dereferencing, and na to nb or nc work by adding & or &mut operators. In terms of types, we arrive at 2a because Vec<i32>: Deref<Target = [i32]> is implemented.
Now we find the first one of these types that has a chunks method. The chunks method we’re after is part of a impl<T> [T] block and a &self method. So self is of type &[T], which matches 2b in the list above with T == i32. We’ll skip the part where no other (in-scope) chunks method exists for any (earlier) type in the list. To get to 2b, the step is thus:
- one dereferencing from
v: Vec<i32> to *v: [i32], and
- one referencing/borrowing from
*v: [i32] to &*v: &[i32]
Finally, we can talk about how to desugar the expression &*v that we arrived at.
The first key insight to understand what’s going on without getting confused is that *v isn’t a value that we could use directly and e.g. assign to a variable. Instead it’s only a “place”, or put differently, the expression “*v” is a “place expression”; value vs. place is a distinction somewhat similar to the concept of “rvalue” vs. “lvalue” in C++.
Now that that’s out of the way, we’ll start actually considering the desugaring of *. Unless we’ve managed to do nothing with it at all, a place such as the one produced by *v is accessed somehow. In this case, the access is by borrowing it immutably via the & operator. More complex accesses could however also involve creating some more places before we access the result. Something like &(*foo).bar to access a field or &mut (*foo)[42] doing an index operation are just 2 possibilities, and trying explain every case can get technical. Anyways, this is immutable access clearly, so we desugar *v using the rule for immutable access, aka a so-called “immutable place expression context”. This way, *v becomes *Deref::deref(&v).
Put together now, &*v thus becomes &*Deref::deref(&v). Note how the desugaring of * itself contained yet-another usage of *, but this usage of * was for dereferencing &[i32] to [i32]. Dereferencing of &T or &mut T (or Box<T>) will not be further desugared, because otherwise we’d never stop desugaring
, instead this operation is supported directly by the compiler as a primitive operation.
Finally, it’s out last job to interpret the meaning of any remaining primitive dereferencing expressions. In this case, that’s fortunately really easy, since we do &*… on a value of type &T. This operation is essentially a no-op, so we might as well just remove it entirely. Thus we arrive at Deref::deref(&v) for &*v, and thus v.chunks() is desugared to <[i32]>::chunks(Deref::deref(&v)).
With all this reviewed in detail, I hope you can appreciate what I meant by my discussion at the beginning of this post about how such a dereferencing operation can both be thought of in terms of converting Vec<T> to [T] or &Vec<T> to &[T], depending on how you look at it, since both interpretations played a role in the desugaring process.
Looking at the overall result, Deref::deref(&v) starts with Vec<T> (though not as a value but as a place, so the Vec is not moved ^^), borrows it (which adds a level of indirection), and dereferences it (which removes one level of indirection), so the overall effect is that, actually, the resulting &[T] does have the same number of indirection to the underlying T as the Vec<T> did. Thinking about having a v: Vec<i32> on the stack, and producing the temporary value &*v: &[i32] to put onto the stack, too, that operation will in fact (after inlining) not be any dereferencing/loading of addresses anymore at all in the assembly, but simply copy a pointer and a length. Except that it loads and stores to access the stack. If these values are however in registers (a Vec fits into 3 registers, and a &[T] into 2 registers), then it really is just some copying after all.
Also note that while for Vec or String, the Deref::deref operation does remove a level of indirection, this doesn’t have to be the case for Deref implementations in general. The only (partial) example of this in the standard library is for Cow<'_, T>, where dereferencing a Cow<'_, i32> for example will, in case the Cow is of the Cow::Owned variant, convert &Cow<'_, i32> to &i32 in the deref implementation which is just going to be adding an offset (assuming the enum tag comes before the payload) to the pointer that represents the &Cow<'_, i32> at run-time.