However, if Foo suppresses Copyable, exclusive access is enforced again, similarly to the first example:
struct Foo: ~Copyable {
var count = 0
func with(_ body: () -> Void ) { body() }
mutating func main() {
with {
count += 1 // overlapping accesses to 'self.count', but modification requires exclusive access; consider copying to a local variable
}
}
}
In this last case, the compiler’s error is slightly different: Swift complains about access not to self, but self.count specifically.
What is the “correct” behavior here? In my opinion it seems like the ~Copyable case should be treated just like the copyable one, but for whatever reason it isn’t, and I don’t see the difference clearly documented in any proposals.
I would try my best to understand the situation, especially where copies happen: non-mutating self.with is called on self, and it executes a closure that directly mutates self.count. A mutating call from non-mutating context is allowed because the caller can be an implicit copy, equivalent to:
struct Foo {
var count = 0
func with(_ body: () -> Void) { body() }
mutating func main(_ target: Int = 10) {
let implicitSelfCopy = self
implicitSelfCopy.with {
self.count += 1
print("Count is now \(self.count)")
self.main(target)
}
}
}
But the reverse is not true: If the calling context is mutating, even reads cannot be safely performed on a copy because it is potentially mutated. So the following code fails to compile:
struct Foo {
let count = 0
mutating func with(_ body: () -> Void) { body() }
mutating func main() {
with { // Overlapping accesses to 'self', but modification requires exclusive access; consider copying to a local variable
print("Count is now \(count)") // Conflicting access is here
}
}
}
Back to the non-copyable case, we could expect that if a context has mutating access, then any other context should have no access to the same struct. Only multiple non-mutating (borrowing) access is allowed at the same time. In fact, if with is not a member of Foo, the pattern just works fine:
My only question on this interpretation is that there is no way to be sure that it really involves an implicit copy. I makes a minor change to the original code and it still works. However, since with takes a borrowing self, it isn't allowed to make an implicit copy of it.
So I suspect what's more likely to happen is the following. self1 and self2 are pseudo code. self1 stands for the inout self parameter main takes. self2 stands for the borrowing self parameter with takes.
struct Foo {
var count = 0
func with(_ body: () -> Void) { body() }
mutating func main(_ target: Int = 10) {
self2.with {
self1.count += 1
print("Count is now \(self1.count)")
self1.main(target)
}
}
}
I think Swift should provide users a way to debug these things rather than leave us to guess about it (I think tools like consume and isKnownUniquelyReferenced(_:) aren't helpful in most cases).
EDIT: sorry, I made a mistake. Changing with to take a borrowing self only affects the code in its own body. It's fine to create an implicit copy in main. Actually SE-0377 explicitly states that an implicit copy is made in this case:
A value would need to be implicitly copied if ..., or while a borrowing or mutating operation is simultaneously being performed on the same binding
If I change main to be consuming, it fails to compile as expected.