rustlifetimeunsaferaw-pointer

How does Rust infer lifetime while raw pointers are involved?


struct MyCell<T> {
    value: T
}

impl<T> MyCell<T> {
    fn new(value: T) -> Self {
        MyCell { value }
    }
    
    fn get(&self) -> &T {
        &self.value
    }
    
    fn set(&self, new_value: T) {
        unsafe { 
            *(&self.value as *const T as *mut T) = new_value; 
        }
    }
}

fn set_to_local(cell: &MyCell<&i32>) {
    let local = 100;
    cell.set(&local);
}

fn main() {
    let cell = MyCell::new(&10);
    set_to_local(&cell);
}

When calling cell.set(&local), suppose cell is 'x and '&local is 'y, I am told that the covariance rule will change the type of cell from &MyCell<'x, &i32> to &MyCell<'y, &i32>.

How does the assignment inside the unsafe block affect the lifetime inference for the parameters of set()? Raw pointers does not have lifetime then how does the compiler know it should make cell and new_value have the same lifetime using covariance?


Solution

  • How does the assignment inside the unsafe block affect the lifetime inference for the parameters of set()?

    It doesn't — and this is not even specific to raw pointers or unsafe. Function bodies never affect any aspect of the function signature (except for async fns which are irrelevant here).

    Raw pointers does not have lifetime then how does the compiler know it should make cell and new_value have the same lifetime using covariance?

    It sounds like you misread some advice. In the code you have, Cell<T> is invariant in T, but for an interior-mutable type to be sound, it must be invariant (not covariant) in the type parameter. In the code you have, the compiler infers covariance for T because MyCell contains a field that is simply of the type T. Covariance is the “typical” case for most generic types.

    Therefore, the code you have is unsound because MyCell is covariant over T but must instead be invariant over T.

    Your code is also unsound because in the implementation of set(), you're creating an immutable reference to a T, &self.value, and then writing to its referent. This is “undefined behavior” no matter how you do it, because creating &self.value asserts to the compiler/optimizer that the pointed-to memory won't be modified until the reference is dropped.

    If you want to reimplement the standard library's Cell, you must do it like the standard library does, with the UnsafeCell primitive:

    pub struct Cell<T: ?Sized> {
        value: UnsafeCell<T>,
    }
    

    UnsafeCell is how you opt out of &'s immutability guarantees: creating an &UnsafeCell<T> doesn't assert that the T won't be mutated. It is also invariant in T, which automatically makes the containing type invariant in T. Both of these are necessary here.