Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does std::rc::Rc need PhantomData?

Tags:

rust

I know that PhantomData is intended to consume a lifetime or type parameter in a data type definition that would otherwise go unused. I was recently looking over the definition of Rc in the Rust std lib and noticed that it seems to employ PhantomData, but it looks like T is being used in the sibling field ptr as an NonNull<RcBox<T>>. The docs say that NonNull is "*mut T but non-zero and covariant." and go on to further extend that definition with the statement:

Unlike *mut T, NonNull<T> is covariant over T. If this is incorrect for your use case, you should include some PhantomData in your type to provide invariance, such as PhantomData<Cell<T>> or PhantomData<&'a mut T>.

Hence, is it needed for variance or is it more because NonNull is effectively a raw pointer and PhantomData is needed to consume the elided lifetime as this answer seems to imply?

like image 340
rjs Avatar asked Sep 20 '19 11:09

rjs


1 Answers

PhantomData is used here to tell the drop checker that dropping Rc<T> may cause a value of type T to be dropped.

When we announce that we may drop a value of type T, the drop checker ensures that any lifetimes in T outlive the struct itself. It is this check that prevents the following code from compiling. In this case, the generic argument to Rc is PeekOnDrop<&'a u8>, which has lifetime 'a.

use std::{fmt, rc::Rc};

struct PeekOnDrop<T: fmt::Debug>(T);

impl<T: fmt::Debug> Drop for PeekOnDrop<T> {
    fn drop(&mut self) {
        println!("{:?}", self.0);
    }   
}

struct SelfReferential<'a> {
   value: Box<u8>,
   rc: Option<Rc<PeekOnDrop<&'a u8>>>,
}

fn main() {
    let mut sr = SelfReferential {
        rc: None,
        value: Box::new(1),
    };

    sr.rc = Some(Rc::new(PeekOnDrop(&*sr.value)));

    // `sr` would be dropped here, which could drop `value` before `rc`.
    // The destructor of `PeekOnDrop` would then try to inspect the (dangling)
    // reference, resulting in UB!
}

For a full explanation of the underlying logic here, see the nomicon, but note that without PeekOnDrop, the previous example compiles just fine. This is because Rc<T> declares in its Drop impl that its generic parameter T is #[may_dangle]. In doing so, it promises that its Drop impl does nothing with the value of T that it owns except (maybe) drop it. Only when the drop checker recursively checks the Drop impl of PeekOnDrop and finds that it may access T does an error occur.

For completeness, here's an example of an undefined program that lies by asserting that PeekOnDrop's Drop impl does not access T using #[may_dangle]. The same undefined behavior would be exhibited in the original example if Rc did not use PhantomData to declare that it may drop a value of T.

like image 128
ecstaticm0rse Avatar answered Nov 09 '22 10:11

ecstaticm0rse