I tried to implement a tree with copy-on-write behavior when I stumbled upon a strange reference counting inconsistency with UnsafeMutableBufferPointer and UnsafeMutablePointer :
class A {
deinit {
print("deinit")
}
}
let p = UnsafeMutablePointer<A?>.allocate(capacity: 1)
p.initialize(to: nil)
p.pointee = A()
print("isKnownUniquelyReferenced: \(isKnownUniquelyReferenced(&p.pointee))")
print("deinitialize")
p.deinitialize(count: 1)
print("deallocate")
p.deallocate()
print("end")
As expected, this prints the following:
isKnownUniquelyReferenced: true
deinitialize
deinit
deallocate
end
However, when we use the subscript instead of pointee:
let p = UnsafeMutablePointer<A?>.allocate(capacity: 1)
p.initialize(to: nil)
p[0] = A() // This line has changed
print("isKnownUniquelyReferenced: \(isKnownUniquelyReferenced(&p.pointee))")
print("deinitialize")
p.deinitialize(count: 1)
print("deallocate")
p.deallocate()
print("end")
then the following is printed:
isKnownUniquelyReferenced: false
deinitialize
deallocate
end
deinit
Firstly, isKnownUniquelyReferenced() returns false. I would have expected it to return true just like in the previous case. Secondly, the instance of A is only deinitialized at the end instead of at the call to deinitialize(). The same behavior can be seen with UnsafeMutableBufferPointer. I have used a Playground in Xcode 13.2.1 on a MacBook Pro with an M1 Max for testing.
Can anyone explain this to me? Have I made a mistake? Is this a bug?
isKnownUniquelyReferenced: true
deinitialize
deinit
deallocate
end
for both variations*.
Note that isKnownUniquelyReferenced is allowed false negative (the reference is unique, but not known to be so). That said, this example should be simple enough for the compiler not to play that card.
It's somewhat off-topic, but there's also a recent effort to shift the lifetime of a variable from the last usage to the end of its scope. That doesn't apply here, but it might be helpful if you're dealing with precise deinit invocation.
* Compiled using
swift-driver version: 1.26.21
Apple Swift version 5.5.2 (swiftlang-1300.0.47.5 clang-1300.0.29.30)
Target: x86_64-apple-macosx12.0
To be pedantic and careful here, the function is allowed false negatives. A false positive can only occur if there has already been an exclusivity violation.
EDIT: ugh, or in the presence of unowned or Unmanaged references.
For the actual question, I suspect the answer is because of “I used a playground for testing”. Playgrounds collect intermediate values for printing and inspection, and the mechanism of this collection sometimes results in values being saved as temporary debugging variables (like when you run an expression in LLDB and want to refer to it later). So playgrounds should never be used for performance tests or exact deinitialization order. In this case, I suspect the playground is recording the value of p[0] as part of the assignment but doesn’t do the same for p.pointee.