We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Unsafe and Soundness step 19 of 24
`PhantomData`, variance and drop-check
pub struct MyIter<'a, T> { ptr: *const T, end: *const T, _marker: ??? }
pub struct MyIterMut<'a, T> { ptr: *mut T, end: *mut T, _marker: ??? }
pub fn iter_script(ops: Vec<String>) -> Vec<i64>
The starter hands you a working MyVec<T>. Write its two iterators — a
shared-borrowing MyIter and a mutable-borrowing MyIterMut — with the
right PhantomData in each, plus iter()/iter_mut() and the two
Iterator impls. Then run a script over a MyVec<i64>:
| op | emits |
|---|---|
push N |
the new length |
sum |
the sum, via iter() |
double |
doubles every element via iter_mut(), then emits the new sum |
max |
the largest element, or -1 if empty |
count |
the number of elements, counted by consuming iter() |
len |
the length |
| anything else |
-2 |
The starter does not compile: E0392, twice. That error is the first of the
three jobs PhantomData does.
Three jobs, constantly conflated
1. Lifetime binding
A struct holding *const T and a lifetime parameter 'a that appears nowhere
else is rejected outright:
error[E0392]: lifetime parameter `'a` is never used
and the error is doing you a favour. If rustc did accept it, 'a would be
unbounded — the caller could instantiate it with anything, including
'static, and your iterator would happily outlive the vector it borrows from.
PhantomData<&'a T> ties 'a to the data.
2. Variance
This is the part that produces unsoundness rather than a compile error, and it is the hardest class of bug in the language to see: the code compiles, it runs, and it produces a dangling reference from entirely safe code.
The table you need:
| type |
variance in T |
|---|---|
&'a T |
covariant in 'a and in T |
&'a mut T |
covariant in 'a, invariant in T |
*const T |
covariant |
*mut T |
invariant |
fn(T) |
contravariant in T |
fn() -> T |
covariant in T |
Cell<T>, UnsafeCell<T> |
invariant |
Covariant means F<'long> can be used where F<'short> is wanted — shrinking
a lifetime is always safe when all you can do is read. Invariant means no
substitution is allowed in either direction.
If your container hands out
&mut T, it must be invariant inT.
Otherwise a caller can shrink the lifetime on the way in, write a short-lived
reference into a long-lived slot through the &mut, and read it back after the
short lifetime has ended. Using PhantomData<&'a T> on a mutable iterator
is a real, exploitable unsoundness — not a style nit.
3. Drop-check
Everybody has read that PhantomData<T> is needed so the drop-checker knows
your type owns a T. Since RFC 1238 that is no longer true for the common
case: a type with a Drop impl is already assumed to own its generic
parameters, so PhantomData<T> adds nothing for dropck alone.
The exception is std’s #[may_dangle] opt-out, which lets a container promise
its destructor will not look at the Ts it drops — and may_dangle is
nightly-only, so the concept can be taught here but the feature cannot be used.
::: question Why is the mandatory assert_variance function the only way this
grader can check variance at all?
Because variance is a type-level property. It has no runtime
representation, so no test can observe it — but a function whose body is
just x type-checks if and only if the conversion is legal.
pub fn assert_variance<'long: 'short, 'short>(x: MyIter<'long, i32>)
-> MyIter<'short, i32> { x }
With PhantomData<&'a T> this compiles: a shared-borrowing iterator may shrink
its lifetime. Swap in PhantomData<&'a mut T> or PhantomData<*mut T> and it
stops compiling, because those are invariant in 'a… in T, at least — and
&'a mut T is still covariant in 'a, which is why the mutable iterator can
also shrink its lifetime while remaining invariant in its element type.
Turning a type-level property into a compile-or-not gate is a technique worth
stealing. In a real crate you put these assertions in a tests/ file or behind
#[cfg(test)], and they cost nothing at runtime while catching a whole class of
regression that no unit test can reach.
The mirror image is the assertion that must not compile:
// must NOT compile -- MyIterMut is invariant in T
// fn bad<'a>(x: MyIterMut<'static, &'static str>) -> MyIterMut<'a, &'a str> { x }
Uncomment it locally and confirm rustc rejects it. If it compiles, your mutable iterator has the wrong marker. :::
The practical rule for this track’s types
-
owning container (
MyVec<T>) →PhantomData<T>, if you need one at all; -
shared-borrowing iterator →
PhantomData<&'a T>; -
mutable-borrowing iterator →
PhantomData<&'a mut T>.
And one more, worth knowing because it is occasionally exactly what you want:
PhantomData<*const T> and PhantomData<*mut T> also strip Send and Sync,
because raw pointers are neither. That is how you make a handle thread-local by
construction.
::: question MyIterMut::next returns Option<&'a mut T> — a reference tied
to the iterator’s lifetime, not to &mut self. How is that not two live
&mut to the same element?
Because the pointer advances before next can be called again, so no element
is ever handed out twice.
This is the fundamental trick behind every mutable iterator in std, and it is
genuinely unsafe: the signature promises the caller a reference that outlives
the &mut self borrow, which the compiler cannot verify. What makes it sound
is a property of the implementation — each element is yielded at most once,
and the elements are disjoint — expressed to the compiler as a raw-pointer
walk that the borrow checker never sees.
Note the shape here, because it is the shape of the whole track: a safe signature, an internal argument the compiler cannot check, and a safety comment that states the argument. The signature is the promise; the pointer arithmetic is the proof. :::
Lints to expect
clippy::needless_lifetimes if you write fn iter<'a>(&'a self) -> MyIter<'a, T> where MyIter<'_, T> would do. clippy::multiple_bound_locations if you
split a bound between the angle brackets and a where clause.
clippy::non_send_fields_in_send_ty if you ever add an unsafe impl Send to
one of these.
What this grader cannot check
Variance is checked, by assert_variance — that is the point of shipping it.
What is not checked is the aliasing argument inside MyIterMut::next: an
implementation that forgets to advance the pointer hands out the same &mut
twice, which is undefined behaviour, and this harness would simply see the
wrong sum. Some bugs of that class show up as wrong answers; some do not.
Remember the grade is compile + tests + clippy -D warnings.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.