Another interesting angle to look from is the question of âwhy are there so many stepsâ. The example in question of turning &&T
into &T
should â if you think about the hardware level â really only take one step, shouldnât it? Why is it 3 steps, removing an indirection twice, then adding back one?
There are multiple answers to multiple aspect of these considerations.
First off, as people who care about performance, does the hardware do some unnecessary extra steps here? No! (Not even without optimizations.) The reason is that Rust at a language level has the concept of âplacesâ in addition to âvaluesâ. If you have a pointer value like p: &mut i32
, then an expression like *p
on its own doesnât do anything yet at run-time, on its own. It simply denotes the âplaceâ that the pointer points to. If you then read from this place, only then would you get something like a mov
instruction with a pointer argument reading from RAM. So something like let x = *p;
actually does a pointer dereferencing at run-time. However if you just take a pointer to the place again, thatâs no longer the case. Writing let q = &*p;
, which turns your &mut i32
into a &i32
, this combination of dereferencing and borrowing corresponds to no operation at all at run-time.
The Borrow<T> for &T
implementation above thus has &*
in the front doing essentially nothing (at run-time), and only the inner/right *
operation corresponds to a read from memory (because itâs a place that is actually used by-value).
Second, as people who care about ergonomics, this &**self
expression business seems a bit tedious to write, doesnât it? If itâs just a single dereferencing, why canât we just write *self
? Answer: we can! Follow-up question: Why doesnât the standard library do that? Answer: I can only speculate, but it might be for being explicit and consistent.
Let's start with consistency: The thing that this Borrow
implementation is consistent with is the corresponding Borrow
implementation for &mut T
(and with the similar BorrowMut
implementation of &mut T
). This one couldnât just use *self
because that would have type &mut T
, not the expected &T
, oh wait it can, but something else is going on here⌠implicit coercions and/or reborrows, leading us to expliciness in the next paragraph. The implementation of Borrow
for &T
does work by just writing *self
to turn &&T
into &T
because reading from the &T
place that *self
denotes by-value can just copy (via Copy
trait) the reference; the same is not true e.g. for &mut &mut T
to &mut T
conversion (used for &mut T: BorrowMut<T>
).
Next up, explicitness. Rust language design acknowledges that often you donât really care about the exact number of indirections, and donât want to explicitly play the game of finding the correct operator soup for the correct type conversion. After all, conversions from &&T
to &T
or &&mut T
to &T
or &mut &mut T
to &mut T
, etc.. have only one reasonable implementation anyways. In any case, Rust can do these conversions implicitly âcalled âderef coercionââ, so all these implementations could indeed just write self
and itâd work! However, for some reason, the standard library authors seem to have preferred the explicit approach here, which is why youâll find &**self
(or &mut **self
) in these trait implementations.