Hi everyone!
I have been tinkering with non-copyable types recently and have run into an issue which I believe is a bug in the current implementation.
I have a class with a mutable stored property of a non-copyable type and I can't seem to consume it to then replace it by another one:
struct NCType: ~Copyable {
consuming func drop() {
}
}
class ExampleClass {
private var ncType: NCType = NCType()
func workaround(_ nct: inout NCType) {
nct.drop()
nct = NCType()
}
func test1() {
workaround(&ncType)
}
func test2() {
ncType.drop() // Error: Field 'self.ncType' was consumed but not reinitialized; the field must be reinitialized during the access
ncType = NCType()
}
}
test1() works as expected, but replacing the stored property directly fails like in test2() with the error message: Field 'self.ncType' was consumed but not reinitialized; the field must be reinitialized during the access
I think the compiler is complaining that you are calling drop while ncType remains uninitialized (it was consumed in the preamble to calling drop). The compiler has no way to know if calling drop will cause ncType to be accessed, so it complains.
With the swap workaround, there is no uninitialized variable at any time, so calling drop is no problem. With the inout workaround, the variable is protected with an exclusive mutable access during the call to drop, meaning reaching ncType during drop would become a fatalError. But with test2 you have potentially reachable uninitialized memory, and this makes calling drop unsafe.
Perhaps as a convenience Swift could treat the uninitialized state as a shared access (like with inout) for the duration ncType is left uninitialized.
Thank you for the reply. I don’t think I get it, the call to drop() on ncType is what consumes it and should be what leaves it in an unitialised state, it doesn’t make sense to me that it becomes uninitialised before that.
I get a different but related error on the next simpler snippet which I also think should be possible:
class ExampleClass {
...
func test3() {
let ncType = self.ncType // Error: 'unknown' is borrowed and cannot be consumed
self.ncType = NCType()
}
}
When you call drop, a consuming function, the first thing that happens is that ncType is "moved" to a temporary location, then drop() is called on that location.
Moving the value from ncType to a temporary location means ncType is considered uninitialized.
Calling drop while leaving ncType uninitialized property means your uninitialized ncType property is reachable during the call. For instance you could have a global variable with an ExampleClass and then try to read it's ncType inside drop.
(As an optimization, the compiler might skip the move and "reuse" the "uninitialized" memory in ncType for the call to drop, or it might not. You can't count on that. Small structs are likely to be passed around in registers for instance.)
I noticed that too. The error message makes not sense, so it's a bit hard to figure out. Make the property final and it makes more sense, only because now the property name is mentioned. I think the error has something to do with how classes implement properties so they can be overridden in subclasses. When the property or the class is final I think it should be allowed, otherwise it'd need some sort of consuming accessor.
In any case, you can easily use swap for this. But I think you're right to say class properties seem to have some issues with non-copyable types.
Thank you for the clarification, I didn’t think of that possibility.
I cannot think of a way Swift could solve that or make it less thorny for classes, unfortunately. I will use pair in the meantime, much appreciated!