The thing that makes Index
special is that self[index]
does not evaluate to a value like a method call does; instead, it evaluates to a place. (In C++ terms, these would be an rvalueC++ and an lvalueC++, respectively.) To make the difference obvious: you can never write self.method() = 0;
, but you can write self[index] = 0;
. (This split requires mut
access to demonstrate, but applies the same to shared access.)
self[index]
desugars[1] to either *Index::index(&self, index)
or *IndexMut::index_mut(&mut self, index)
depending on how the place is used -- if it's used in a way that requires mut
access, IndexMut
is used; otherwise Index
is used. This is why the equivalence is self.index(index)
and &self[index]
.
In C++, this behavior comes from operator[]
conventionally returning T&
C++: an lvalue referenceC++. Rust does not have an equivalent to C++'s lvalue reference, and imho this is a good thing, because Rust's references are just normal types/values that behave like any other type/value, whereas C++'s references behave distinctly differently from nonreference types[2].
Rust's a[b]
syntax will always evaluate to a place, just like *a
evaluates to a place despite Deref::deref
returning a reference. Nobody's really asking for Deref
to be able to return things other than a reference[3], so what makes Index
any different?
Where Index
[Mut
] do fall short and could be improved is that the existing traits only cover two of the four ways that a place can be used. A place can be used by-ref (&place
), by-ref-mut (&mut place
), by-move ({place}
), or as the lhs of an assignment (place =
).
Additionally, it would be nice if indexing could return reference wrapping types like RefMut
) -- and with some use of temporary lifetime extension, this is actually possible to do.
The most general that Index
[Mut
] may be in the future would probably look something like this:
trait Index<Ix: ?Sized> {
type Output: ?Sized;
type Ref<'a>: Deref<Target=Self::Output> = &'a Self::Output;
fn index(&self, ix: Ix) -> Self::Ref<'_>;
}
trait IndexMut<Ix: ?Sized>: Index<Ix> {
type RefMut<'a>: DerefMut<Target=Self::Output> = &'a mut Self::Output;
fn index_mut(&mut self, ix: Ix) -> Self::RefMut<'_>;
fn index_set(&mut self, ix: Ix, val: Self::Output) {
*self.index_mut() = val;
}
}
with the desugarings of
& |
self[index] |
as | & |
*Index::index(&self, index) , |
&mut |
self[index] |
as | &mut |
*IndexMut::index_mut(&mut self, index) , and |
self[index] = value |
as |
IndexMut::index_set(&mut self, index, value) . |
Note, however, that I do not think that index_set
will ever happen nor that it is a good idea; overloading assignment is something Rust has rightly avoided so far and I think should absolutely continue to avoid[4]. Assignment should be a move should be a simple bitcopy; if you want behavior, use a method.
Note also that IndexMove
is a very awkward trait, even when assuming the presence of something like &move
references. I originally considered including a shape for IndexMove
, but it's extremely unclear how such should even function. We already have "IndexMove
" for all indexables where Output: Copy
, as well as for [T; N]
even when T
isn't Copy
, so whatever shape IndexMove
takes would have to work for both of those. And I don't think that it's possible, unfortunately, to have an IndexMove
which provides the correct semantics. (This is in contrast to DerefMove
, which I absolutely think is something which can be made available to user types.)
Relaxing Index
[Mut
] in this way works because of temporary lifetime extension. Even if the reference wrapper is immediately dereferenced, Rust will hoist it to only drop at the end of scope if it's used -- this scope may be the end of the containing expression[5] or the containing block depending on if its borrow escapes the expression containing the temporary. Here's an example.
-
Very rough desugar, and I'm skimming over auto(de)ref behavior a bit since it's not super relevant here. ↩︎
-
Notably, you cannot nest C++ references, thus
T&&
being yet another different kind (rvalue reference) and the existence ofstd::reference_wrapper
to lift from a reference kind to a normal type kind. ↩︎ -
Although, now that I've said this, I'm basically guaranteed to find someone who unironically thinks this should happen. ↩︎
-
The singular reason I could see something changing is for
Cell
.Cell::set
is still just a simple bitcopy, but working withCell
d data has a significant amount of extra red tape which doesn't need to exist, and Rust would probably benefit from removing somehow. ↩︎ -
There's an existing footgun here: used temporaries in the scrutinee of
match
orif
or other block-like expressions live until after the expression's block, even if the temporary is no longer accessible from within the block. This doesn't get in the way most of the time since "non lexical lifetimes" (NLL) allows regular references' lifetimes (and many struct's lifetimes) to be ended after the last use "prematurely" before the end of scope, but this does not happen if the struct containing the lifetime has any drop glue (implementsDrop
or contains a type with drop glue)[6]. ↩︎ -
There's an unstable opt-out called the "eyepatch" with
#[may_dangle]
, where with a type likeBox<T>
, despite having aDrop
implementation, does not useT
in any way, so any lifetimes inT
are allowed to be NLLd the same as ifT
were held directly on the stack. ↩︎