Is self-move well-defined?

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)
}

As this code seems to produce different results depending on whether optimization is enabled, I'm inclined to believe this is just a bug.

Edit: Actually, it produces the same different results even if I add an a == b check. So, there is a bug, but I'm not sure it's in the self move.

1 Like

I believe that bug is unrelated, as you don't need to call swap to reproduce it :|

Regardless, I've opened a PR in case the guards were unintentionally omitted.

1 Like

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.

2 Likes

I reported the unrelated bug with the deinit: UnsafeMutablePointer Not Calling Deinit of Generic Noncopyable Type in -ONone · Issue #85725 · swiftlang/swift · GitHub

1 Like

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.