Are `(*ptr).A` and `ptr.read().A` equivalent?

I like this chain calling more, but ptr.read() will copy the whole struct, can I assume that the compiler will always optimize?

They are not: the former results in a place that you can read from and write to, and take the address of.

That doesn't work if you are explicitly .read() ing, because that unconditionally moves, and yields a value, not a place.

7 Likes

The read method explicitly creates a new owned value by duplication. If the value has a destructor, this can result in running the destructor too many times if you are not careful.

8 Likes

Technically the place is not always writable, it depends on whether ptr is a *mut or a *const.


With ptr.read().A you can also assign to the resulting expression or take a mutable reference to it, but this will mutate a new anonynous variable and not the value pointed by ptr.

5 Likes

Besides the other listed reasons, reading the whole pointee can also cause UB if the whole pointer & its pointee don't satisfy all validity requirements. For example, this code is correct:

unsafe fn foo(ptr: *mut (u32, u32)) {
    ptr::addr_of_mut!((*ptr).0).write(5);
    let v = ptr::addr_of!((*ptr).0).read();
    assert_eq!(v, 5);
}

while the following code contains UB if (*ptr).1 is uninitialized (reading an uninitialized palce is UB, even if the result is unused):

unsafe fn foo(ptr: *mut (u32, u32)) {
    ptr::addr_of_mut!((*ptr).0).write(5);
    let v = ptr.read().0;
    assert_eq!(v, 5);
}

Similarly, ptr could be unaligned, even if (*ptr).A is aligned (since the field has some offset from the struct's beginning).

EDIT: fixed code

4 Likes

Can you write through a *mut (u32, u32) that way?
Playground of your examples

Edit: I may have misunderstood, if that was intended as pseudocode for offsetting the pointer to each field.

Yeah, sorry. I should have checked the code before posting. Here is how it should look.

2 Likes

(Disclaimer: I am a member of T-opsem. I am not intending to speak normatively, but I did double check the reference. The reference book isn't fully normative either, but it's intended to be accurate, and the section I link was added after T-opsem FCP.)

If ptr is misaligned, (*ptr).A is considered based on a misaligned pointer. Directly reading/writing from/to such a place that is based on a misaligned pointer is UB, and you MUST first place it behind a pointer again if you want to access it validly.

You did correctly do so (after the fix to not be calling the pointer methods on the place), so your example is fine, but this is still worth pointing out. An interesting consequence of the rules is that it's valid to write your example as[1]

unsafe fn foo(ptr: *mut (u32, u64)) {
    *&mut (*ptr).0 = 5;
    let v = *&(*ptr).0;
    assert_eq!(v, 5);
}

since the reference-of operators only require the field to be aligned, not for the place to be based on an aligned pointer. However, I would recommend not writing this, since relying on *& to be semantically meaningful is unreasonably subtle and clippy will suggest you remove it.

Just use addr_of! instead, as that's much clearer on intent. That *& can be semantically meaningful is an intellectual curiosity at best[2].

NB: making it clearer that this is a property of places and that addr_of! doesn't magically compute its argument place with weaker semantics is a main motivating factor behind the FCP to stabilize &raw.


  1. Since I'm being pedantic, I better point this out as well: my rewrite introduces a drop of the old value in the place, whereas ptr::write overwrites the value without dropping it. In this case there's no semantic difference, but that relies on formally non-guaranteed properties (i.e. that uninitialized behind &mut isn't immediately UB and that dropping Copy types is a full no-op that doesn't assume value validity). ↩︎

  2. Much more reasonable of an application is just &(*ptr).0 on its own, which sees no benefit from requiring ptr to be fully aligned, nor would writing &*addr_of!((*ptr).0) to lower the implied alignment. That *& can be meaningful just falls out of & only requiring what it actually needs, and that loading (*ptr).0 with the alignment of *ptr is beneficial (e.g. an alignment raising newtype), thus is required. ↩︎

Interesting. I didn't know about that rule, and used read/write just to avoid subtleties around drops of pointer contents. Turns out that's not the only reason why using those functions instead of direct assignment is safer.

Given the amount of footguns around direct pointer value assignment, perhaps it would be better to deprecate it entirely, or at least lint against them, so that pointer contents are accessed only through read/write?

I'm surprised the misaligned provenance rule actually exists. Is there a reason for it, or is it just LLVM's semantics leaking through? Opsem (or at least Ralf) seem generally opposed to UB which doesn't enable optimizations, and I find it hard to imagine how the misaligned provenance rule can help with optimization. At least the validity of contents for &/&mut seem like a much more profitable guarantee, and yet it is suggested to drop that requirement.

1 Like

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.