A solid chunk of this discussion seems to gloss over the basics: the CPU instructions that end up being executed. Your source code is lexed into tokens, parsed into an AST, lowered into an HIR, into THIR, into MIR, into LLVM's IR; codegen'ed/linked into an executable file you get to run. By the end, it has no "clue" what a "reference" even is, much less how to "create" it.
At assembly level, there is no concept of "taking a reference", or "dereferencing a pointer", or even "assigning" an x into the y via a let y = x or similar. You can only "copy" individual bytes: from the stack into a register, from the register back onto the stack, from the stack to a given address in memory (RAM), from memory back into the register.
A typical let x = y assignment translates into "copy all the bytes from the register/memory address 'assigned' to y into the register/memory address 'given' to x". That's it.
If the y is a ZST, no copying takes place. (Can you guess why?)
fn main() {
type ZST = ();
let ptr_zst = std::ptr::null() as *const ZST;
unsafe {
let zst = *ptr_zst; // no UB
assert_eq!(zst, ());
}
}
If the y is zero-length slice, no copying happens.
fn main() {
let ptr = std::ptr::null() as *const String;
let ptr_slice = std::ptr::slice_from_raw_parts(ptr, 0);
unsafe {
let slice = &*ptr_slice; // no UB
let mut vec = Vec::new();
vec.clone_from_slice(slice); // no UB either
assert!(vec.is_empty());
}
}
Any typical let x = y assignment is, for our intents and purposes, a plain std::ptr::copy:
fn main() {
let x = 1234;
let y = x;
// translates into:
let mut y: i32;
unsafe {
// conceptually speaking
std::ptr::copy(&x, &mut y, 1);
}
}
Any let x = *y no longer needs to take the address of y. It uses the value it holds:
fn main() {
let x = *y;
// translates into:
let mut x;
std::ptr::copy(y, &mut x, 1);
}
Any let _ = *x removes the destination from your std::ptr::copy entirely. Without knowing where the bytes located at the address in the x must go; no copying can take place. Thus:
fn main() {
let ptr = std::ptr::null() as *const i32;
unsafe {
_ = *ptr;
let _ = *ptr;
// both of these *could* translate into
// an invalid operation the compiler would need
// to manually throw away in one of it's lowering steps
std::ptr::copy(ptr, <n/a>, 1);
// yet it is (usually) smart enough
// to infer that `_ = x` means "evaluate x,
// "then forget about it altogether". so it does
// *nothing* with the *address* it "reads from" the `ptr`,
// and no UB can ever happen as a result of that
}
}