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

41 Views Asked by At

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.

2

There are 2 best solutions below

0
drewtato On BEST ANSWER

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.

0
cdhowie On

unsafe { *cell.get() } evaluates to an i32. It copies the value out of the cell into a function-local temporary, then you try to return a mutable reference to that temporary with &mut.

&mut *cell.get() performs a reborrow of the pointer target.

The constructs &* and &mut * perform reborrowing (possibly after deref coercion, though that does not occur here). Syntactically, this requires the dereference and the borrow not be broken up. In the first example, the unsafe block is between them, but this is not the only construct that breaks up the reborrow pattern; you will get the same error if you do this:

unsafe { &mut { *cell.get() } }

The error is a bit helpful in demonstrating what's happening if the cell contains a type that isn't Copy:

fn get_ref_compiles_fine<T>(cell: &UnsafeCell<T>) -> &mut T {
    &mut unsafe { *cell.get() }
}
error[E0507]: cannot move out of a raw pointer
 --> src/main.rs:4:19
  |
4 |     &mut unsafe { *cell.get() }
  |                   ^^^^^^^^^^^ move occurs because value has type `T`, which does not implement the `Copy` trait

This gives you the hint that the syntax results in an attempted move of the inner value, which therefore causes a copy instead when the cell's inner type allows it.