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 T
s 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.