I am trying to implement Vector following the tutorial here Implementing Vec - The Rustonomicon and had a few questions with the Drain functionality.
Why do we need to specify PhantomData in the layout of Drain ? It seems to be because of lifetime but I am not quite sure why we need to specify lifetime itself. I understand the intent behind it which I believe is to say the values are valid for the lifetime of Drain. But with or without lifetime it compiles and can run the following test
#[test]
fn test_drain() {
let mut p = Test { x : 1, y: 2 };
let mut t = Vector::<Test>::new();
t.push(p.clone());
t.push(p.clone());
t.push(p.clone());
t.push(p.clone());
{
let mut drain_iter = t.drain();
p = drain_iter.next().unwrap();
}
println!("Value of p.x = {}", p.x);
}
In both cases, above test successfully compiles but fails with SIGSEGV error (Invalid memory reference). So is there any value in specifying lifetime/PhantomData in the layout of Drain ?
In case we do need the PhantomData what is the difference between PhantomData<T>; PhantomData<&T> and PhantomData<&mut T> ?
The lifetime prevents access to the Vec while the Drain is alive. If Drain did not have a lifetime at all, then it would not be considered as a borrow of the original Vec, and this can quickly lead to UB in safe code.
The difference between &T and &mut T is more about lifetime variance, since &mut T is invariant in T. It also affects auto-traits like Send and Sync.
The lifetime prevents access to the Vec while the Drain is alive. If Drain did not have a lifetime at all, then it would not be considered as a borrow of the original Vec , and this can quickly lead to UB in safe code.
That makes sense. Thank you. Now it makes even more sense why the PhantomData is on Vec<T>.
The difference between &T and &mut T is more about lifetime variance, since &mut T is invariant in T . It also affects auto-traits like Send and Sync .
Both of them are covariant over 'a but we need &mut Vec<T> in this case because of the constraint on lifetime of T right ?
Yes, although actually it's fine for Drain to be covariant in T as well, because it only ever moves items out of the collection, never writing items back in where such lifetime variance would cause a problem. It's not wrong to be invariant in T, just more limiting, and the standard library implementation of Drain is in fact covariant.
The auto traits are probably a bigger concern -- this is moving items, so Send for Drain should require T: Send, but leaving it auto based on PhantomData<&T> would have T: Sync. The standard library manually implements Send and Sync here.