The point is that the example under discussion isn't a move, it's a copy. You can still get the address of a
, because the copy doesn't invalidate the source place, and if a reference of a
ever escaped, its validity is allowed to continue until a
is used by-mut (possible) or by-move (impossible), so we can't turn the last copy into a move without violating AM semantics.
The refinement here is that it could potentially make sense to treat heap allocation and stack allocation differently, guaranteeing that heap allocated objects always have disjoint addresses, but not guaranteeing that for stack allocated objects.
This would mean ptr_eq
works to compare allocation identity for any heap allocated objects (e.g. Arc::ptr_eq
) but not for potentially stack allocated objects (e.g. <[_]>::eq
, where it could also return true
for distinct allocated objects which are structurally equivalent and thus (potentially) allowed to overlap.
This is weakly related to “target guaranteed behavior” and “unrealized spec nondeterminism;” if the spec allows some nondeterminism (e.g. overlapping addresses allocation), it should ideally be relatively simple to realize that nondeterminism, otherwise it de facto doesn't exist and people will (potentially accidentally) rely on it not being introduced.
Heap move elision is typically done via manual specialization (e.g. vec.into_iter().collect()
) and is difficult to do automatically (because of side effect ordering in the face of divergence, AIUI), but there's no way to do this for stack moves (other than writing code using references instead). This makes realizing overlapping heap allocated objects much rarer than for stack allocated objects, potentially justifying treating them differently.
An even weaker version (though more difficult to specify) would to be allow overlapping objects only for T: Copy
in the source, but guarantee for ?Copy
generics that the address is properly disjoint from any other allocated object.
Implementation-wise, the Rust ABI would have an invisible flag between two kinds of by-reference passing — callee copy/clobbered (today's semantics) and caller copy/maintained, with the latter used only for any by-value arguments which are allowed to have overlapping address allocation.
The specification question can be summarized as is the following snippet allowed to evaluate to true
? Optimizing other examples unifies with this example in the face of analysis-escaping references.
type BigCopy = [u8; LARGE_ENUF];
fn do_args_overlap(a: BigCopy, b: BigCopy) -> bool {
ptr::eq(&a, &b)
}
let data = BigCopy::default();
do_args_overlap(data, data)
Alternatively, the optimization could be made possible by making escaping references not cause simpler examples to unify to do_args_overlap
, by declaring by-copy stack-place usage to invalidate extant pointers, i.e. make the following UB:
let place = 0_i32;
let p = ptr::addr_of!(place);
{place};
&*p;
This seems highly unintuitive. The “don't invalidate pointers” copy could still be written as *&place
... which also suggests the “do invalidate pointers” copy could be written today as *&mut place
.