Perhaps the further messages cleared up what I'm about to write in this top section.
But this sounds like you think that the dynamic liveness scope of Vec<T>
-- where you drop it -- is directly tied to 'a
, due to T: 'a
. But Rust lifetimes ('_
things) do not work like that. T: 'a
means that the type T
contains no lifetimes shorter than 'a
, so the type is valid within the lifetime 'a
. Note that this a restriction on the type, not on any particular value, and that by "lifetime" here I am not talking about liveness scopes -- where things drop.
@Kyllingene put it well:
The main connection between dropping and something like T: 'a
is that T
has to be a valid type when it is dropped if it has a non-trivial destructor. Which means that T
drops "somewhere in 'a
". But it could be anywhere in 'a
. Or put another way: the type of a value has to be valid everywhere it is used, and a non-trivial destructor is a kind of use.
Alright, let me try to go back to some actual code to talk about what the lifetime is about. Here's the Drain
declaration from the Nomicon:
pub struct Drain<'a, T: 'a> {
vec: PhantomData<&'a mut Vec<T>>,
iter: RawValIter<T>,
}
Probably the only reason there's an explicit T: 'a
there is that the example was written awhile ago. Today you could write it like this:
pub struct Drain<'a, T> {
vec: PhantomData<&'a mut Vec<T>>,
iter: RawValIter<T>,
}
This still has the T: 'a
bound, but it's just inferred for you, because the &'a mut Vec<T>
that appears in the fields requires it to be true in order to be valid. You use to have to state the bound explicitly.
So that's the surface reason for the lifetime bound on Drain
: it's required due to the field type.
The reason that the lifetime is present at all connects back to the only place these types are created:
impl<T> Vec<T> {
// I added this to better communicate that the `Drain` holds on
// to an exclusive borrow of `self`
// vv
pub fn drain(&mut self) -> Drain<'_, T> {
This is required so that the compiler knows to keep the Vec<T>
exclusively borrowed so long as the Drain<'_, T>
is around. That's what Rust lifetimes are generally about: the duration of borrows.
Now, in this version that you presented, there's no implicit or required T: 'a
relationship anymore:
pub struct Drain<'a, T> {
_lt: std::marker::PhantomData<&'a ()>,
iterator: RawIter<T>,
}
So within the same module, you could construct a Drain<'static, &'not_static Ty>
. But if the only construction site is the drain
method, this won't actually happen in practice: that method requires T: 'a
implicitly by having a self: &'a mut Vec<T>
receiver.
Any place you could construct a Drain<'static, &'non_static Ty>
wouldn't be connecting the lifetimes of a borrowed Vec<T>
to the Drain
, and thus would probably be broken in other ways. But some of the potential breakage just isn't possible with the unique construction site.
But hold on! There's another purpose to PhantomData
besides "constraining" a generic parameter you don't have as a field (e.g. adding a lifetime that's not otherwise present). And that is to steer variance and auto-traits like Send
and Sync
. (In fact being able to decide those things are why you have to constrain generic parameters in the first place.)
Due to the use of *const T
in the RawIter<T>
, you don't get the auto-traits without an unsafe
implementation anyway (like here), so that's not too bad -- you're forced to think about the soundness of the auto-traits somewhere (by luck of having used *const T
).
However, your code using PhantomData<&'a ()>
, Drain<'_, T>
is covariant in T
, whereas in the nomicon version, it's invariant in T
. The covariance means, for example, you can coerce a Drain<'d, &'long str>
to a Drain<'d, &'short str>
.
I don't think this is actually exploitable in the provided code: the provided interface only exposes T
s in covariant ways (like handing you the T
). And the original Vec<T>
is always empty by the time the Drain<'_, T>
is constructed, and stays that way. But I also believe that's only by pure luck, and undesirable even if sound.
Let's look at how this could lead to UB given a Drain
with a more complete feature set.
Say you could obtain a &mut [T]
from Drain
as described here, and you also had the keep_rest
method. Then you could start with a Vec<&'long str>
, get a Drain<'_, &'long str>
from the drain
method, coerce it to a Drain<'_, &'short str>
, obtain a &mut [&'short str]
, put a short-lived &str
into the slice, and then call keep_rest
. At that point the Vec<&'long str>
becomes usable again, but now it contains a &'short str
. This UB already at the language level, and can quickly lead to things like use-after-free.
Here it is mocked up in the playground.
If I'm correct and the covariance is actually sound in the OP due to not having these features, it still means these features could never be added: going from covariant to invariant is a breaking change.