I noticed, while browsing the standard library, that *Span/swapAt(_:_:) doesn't guard i != j else { return } unlike its peer containers. Is this a bug, or is self-move well-defined in Swift?
My preference is that Swift simply defines away the aliasing problem by declaring that self-move is a no-op regardless of initialization state, because that would reduce the number of tripwires to step over when swapping values. So here I am asking about it before drafting a pull request with guard statements. Is there any documentation that approves of uninitialized self-move operations, or is it just a bug that needs fixing? Imagine the following swapping algorithm:
func swap<Pointee>(
_ a: UnsafeMutablePointer<Pointee>,
_ b: UnsafeMutablePointer<Pointee>,
) where Pointee: ~Copyable {
let c: Pointee = a.move()
a.initialize(to: b.move()) // is this a well-defined no-op if (a == b)?
b.initialize(to: c)
}
Swift forbids accessing storage while you are mutating it, often referred to as the “law of exclusivity”, which means self-moves “cannot occur”. Doing so with pointers is stepping outside what’s supported by the language and is undefined behavior (this is why pointers are “unsafe”!)
EDIT: …but your original question was about swapAt, and I’m not sure what the answer is for that specific case—if it can lead to code like your example, it should indeed be guarding against it.
Is it possible to break it? If not, what is it guarding against? I tried to come up with a real example of breaking an unguarded self-swap before opening this thread, but I failed to do so in a timely manner. My best bet was to cause some reference-counting havoc, but x.initialize(to:) doesn't retain and x.move() doesn't release, so the house claimed that jackpot. I'm currently trying to understand whether the guard serves a real or arbitrary purpose. I understand that reading uninitialized memory is undefined, but you could probably solve that by defining moved-from memory as a secret third thing.