Search code examples
pointersrustlifetimeborrow-checkerunsafe

UnsafeCell getting mutable reference from a function: cannot return reference to a temporary value


I'm experimenting with UnsafeCell and here is an example:

use std::cell::UnsafeCell;

fn get_ref_compile_error(cell: &UnsafeCell<i32>) -> &mut i32 {
    &mut unsafe { *cell.get() } // error: cannot return reference to a temporary value
}

fn get_ref_compiles_fine(cell: &UnsafeCell<i32>) -> &mut i32 {
    unsafe { &mut *cell.get() } // ok
}

fn main(){
    let cell = UnsafeCell::new(1);
    let ref_compiles_fine = unsafe { &mut *cell.get() }; // ok
    let ref_compiles_fine_2 = &mut unsafe { *cell.get() };  // ok
}

Playground

I suspect the reason &mut unsafe { *cell.get() } does not compile is that the unsafe { *cell.get() } probably has the lifetime of the function block scope.

This seems a bit weird to me since borrowing the same object results in different behavior based on if the reference inside unsafe block or not.

QUESTION: Why is it ok to return unsafe { &mut *cell.get() }, but not &mut unsafe { *cell.get() } while this is in fact borrowing of the same object.


Solution

  • Here's a simpler example that shows the difference a little better:

    pub fn unmut_string(s: &mut String) -> &String {
        // this is fine
        &*s
    }
    
    pub fn unmut_string2(s: &mut String) -> &String {
        // this is not
        &{ *s }
    }
    

    The difference concerns place expressions and value expressions. *s is a place expression, and when you apply & to this place expression, you get a reference value. However, blocks {} can only contain value expressions, so it tries to convert the place expression into a value by copying. Only after copying does it try to make a reference to the new value.

    error[E0507]: cannot move out of `*s` which is behind a mutable reference
     --> src/lib.rs:8:8
      |
    8 |     &{ *s }
      |        ^^ move occurs because `*s` has type `String`, which does not implement the `Copy` trait
    
    error[E0515]: cannot return reference to temporary value
     --> src/lib.rs:8:5
      |
    8 |     &{ *s }
      |     ^------
      |     ||
      |     |temporary value created here
      |     returns a reference to data owned by the current function
    

    Note: I swapped the order of these errors

    The first error is because String values can't be copied. Your code doesn't produce this error because i32 can be copied, but that's not what you want. You want to reference the existing value, not a reference to a copy.

    From there, the second error is more clear: (assuming the copy succeeds,) you're trying to create a reference to a value that was just created. When the function returns, that value would be dropped, and the reference would no longer be valid.

    Here's an example where both versions work, and the difference you get as a result. The first one does what you expect, while the second leaves x unchanged. This is what happens in your two // ok lines at the end of main.

    let mut x = 5;
    let r = &mut x;
    let y = &mut *r;
    *y += 1;
    println!("{x}"); // 6
    
    let mut x = 5;
    let r = &mut x;
    let y = &mut { *r };
    *y += 1;
    println!("{x}"); // 5
    

    In this code, the second y is a reference to a value that undergoes temporary lifetime extension. That is, it's equivalent to this code:

    let mut x = 5;
    let r = &mut x;
    let mut _y = *r; // _y is a hidden variable
    let y = &mut _y;
    *y += 1;
    println!("{x}"); // 5
    

    In this code, it should be clear that y can only be valid while _y exists, and that _y is separate from x, which has been copied.

    Relating back to your code, unsafe blocks are a type of block, so they can only wrap value expressions. There's no way to wrap an unsafe place expression by itself, but since place expressions are typically very short, it's easy to wrap the surrounding value expression instead. And really, &mut * is a single operation that casts a raw pointer into a reference.