Reference vs place expression

As I understand it this:

fn foo(x: *mut u32)  {
    *x = 0;
}

Technically forms a place expression to x, not a reference, which is why if you want to return a reference you need to add a &:

fn foo<'a>(x: *mut u32) -> &'a mut u32  {
    *x = 0;
    &*x
}

Reading about unsafe rust it appears creating a reference creates a lot of assumptions in the compiler. Is this also true for place expressions or only references? What are you allowed to do in unsafe code with place expressions but not references or vice versa? I got to thinking about this because a packed struct field access is a place expression and ok, but making a reference to the packed struct field is not b/c of reference alignment requirement, which I guess place expressions don't have?

Without going more in depth, note that creating a place expression requires fewer conditions than creating a reference. E. g. that's why addr_of is a thing. In your example code for example, assigning to *x (since it's target type doesn't have any destructors to run!) would be valid even if the memory location contained uninitialized memory, while creating a reference wouldn't be.

OTOH, of course there's lots of overlap; and also, arguably, crating a place expression alone doesn't do anything at all, but what you do with it is important, creating a reference is one option, assigning or using addr_of are others, and there's more alternatives including reading (i. e. copying/moving out of) the value at that place or using the place expression to build up a longer place expression e. g. to a field.. All of these have different safety requirements when used with a dereference-of-raw-pointer place expression, and they all share the common requirements of the pointer being dereferencable as explained in the docs, and also I suppose some details of the exact requirements for some operations are yet to be determined.

4 Likes

In a recent comment of mine on an issue, I gave a table of the requirements for various operations on pointers (to the best of my understanding). Note that I use "allocated" as a shorthand for "dereferencable". Here's a copy of the table for reference:

Expression Allocated? Initialized & valid? Unaliased?
ptr
(*ptr).field = value (if Copy)
addr_of[_mut]!((*ptr).field)
addr_of[_mut]!((*ptr).field1.field2) (directly)
value = (*ptr).field
(*ptr).field = value (if not Copy) ✓* ✓*
&[mut] (*ptr).field
(*ptr).field.method()
addr_of[_mut]!((*ptr).field1.field2) (through Deref[Mut])

* unless field has no subobjects with nontrivial drop code

So we can read from a place expression, write to it, or produce a pointer, but calling a method or dropping a previous value requires a reference.

3 Likes

And, apart from the high-level semantic meaning, one could argue that place expressions and references don't differ in much when it comes to low-level implementation, either. A place expression is roughly something that can be addressed, because (before optimization and register allocation) addresses must be used for accessing memory, since that's how memory works.

So even though the programmer doesn't explicitly write any references, such as in x = 42;, the compiler will typically generate code that is effectively equivalent to *&mut x = 42; because ultimately, it's the address that identifies an individual variable/field/etc.

Incidentally, traits that need to "magically" pass through place-ness (or "lvalue-ness" as some other languages call it) will simply turn into functions that accept/return references. The prime example is Index. When one writes slice[index] = value, the expression slice[index] appears to be a place, i.e. it's "the value in the slice at that index itself". However, I put that in scare quotes because it is not how Index works. If <[T] as Index>::index() had the signature

fn index(&self, index: usize) -> T;

i.e. if it returned the element by-value, then it would be impossible to:

  1. take a reference to the element being accessed;
  2. mutate the element being accessed by assigning to it;
  3. (or even impl<T: !Copy> Index for [T] since one can't move out of a reference.)

This is why Index{,Mut} are declared to return &[mut] T and reading/writing an indexed element desugars to *slice.index(i) and *slice.index_mut(i) = new_value, respectively: in order to emulate place-ness, indirection and references must be involved.


All of this, of course, is only to support the point of place expressions and references inherently having a lot in common. As others already explained above, they are not the same when it comes to requirements imposed by the abstract machine.

3 Likes

Does (if Copy) and (if not Copy) apply to the object pointed to be ptr or the field? It's not obvious to me why Copy'ness should change these answers.

Here, I mean that it applies to the field. The reason I separate those out is that Copy types are guaranteed to have a trivial destructor, but !Copy types have no such guarantee in general. Since assigning a value to (*ptr).field drops the previous value, if that previous value has a Drop::drop() method, that method will require a valid, unique &mut self reference. More accurately, one can say that setting (*ptr).field = value with an uninitialized field is sound if std::mem::needs_drop::<T>() is false.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.