As per Rustonomicon
Rust largely understands that any operation that produces or stores a ZST can be reduced to a no-op
on the other hand
Note that references to ZSTs (including empty slices), just like all other references, must be non-null and suitably aligned. Dereferencing a null or unaligned pointer to a ZST is undefined behavior, just like for any other type.
This two paragraphs appeared a bit contradictory to me. Consider the following example:
fn main() {
let null_ptr = ptr::null_mut::<()>();
unsafe { *null_ptr = () }
}
It compiles and runs fine under rustc 1.75.0, but is the behavior of the code well defined?
From one hand it should be no-op, so there's probably no any problems, from the other there's a dereferencing of null pointer which is UB.
Note: The reason I tried to use null pointers here is that allocators might allocate some non-zero sized memory even for ZSTs which in my specific environment is not accepted.
The first statement doesn't talk about semantics - you will see from the context that it talks about performance. ZSTs are efficient because operating on them is no-op. Still, there are requirements for them, arising from the operational semantics of Rust. Your snippet is UB and Miri flags it as such.
The solution to your problem is to use a non-null, aligned, but not allocated pointer for this situation. std::ptr::NonNull::dangling().as_ptr() can easily give you one. Such pointer is valid for ZSTs because it is aligned and non-null (provenance isn't required for ZSTs, although note that currently incorrect provenance can cause UB).
Also note that requesting an allocation of size zero is UB on its own so you must check for it (assuming you use Rust's interface for allocators).
The question as stated will lead to a somewhat frustrating answer because the situation is quite simple: Dereferencing a null-pointer is always Undefined Behavior, full stop. There is no exceptions for ZSTs. The classical "but it runs fine on my platform" is null and void.
The second part of your question is the more interesting one: If you need to special case ZSTs in order to avoid allocations, you may to use a "sentinel address" for those, which the allocator can allocate on first use; keep in mind that ZSTs have alignment just like every other type. In that scenario, all allocations for all values of a ZST point to the same memory location.
Yet I'd rather suggest to check if you can use a different system allocator that already exhibits (or can be configured to exhibit) this behavior.
Also keep in mind that equality and identity are not the same thing. I can have two values of a ZST that compare non-equal, because the underlying equality check relies on the memory address to do the comparison. It's up to the ZST to do this, so your scheme may fail to support those types.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With