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.
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.
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
.
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
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.
(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
.
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 droppingCopy
types is a full no-op that doesn't assume value validity). ↩︎Much more reasonable of an application is just
&(*ptr).0
on its own, which sees no benefit from requiringptr
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.
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.