Non-copyable deinit cannot consume?

I hit into that deinit for non-copyable types is not consuming.

The basic sample would be the following:

import Synchronization 

struct S: ~Copyable {}

final class Storage: Sendable {
    let s: Mutex<S?> = .init(.init())

    init() {

    }    
}

struct SHolder: ~Copyable, Sendable {
    let s: S
    weak var storage: Storage?

    deinit { // cannot add `consuming`
        let s = consume self.s
        self.storage?.s.withLock {
            $0 = s // error: implicit conversion to 'S?' is consuming
        }
    }
}

Is this limitation intentional?

2 Likes

The problem is that the local variable s is consumed within a closure, and the compiler can't guarantee that the closure is only executed once, even though that happens to be the case for withLock.

To resolve the issue, you can use an optional and the Optional.take method to assert at runtime that the closure only executes once:

import Synchronization 

struct S: ~Copyable {}

final class Storage: Sendable {
    let s: Mutex<S?> = .init(.init())

    init() {

    }    
}

struct SHolder: ~Copyable, Sendable {
    let s: S
    weak var storage: Storage?

    deinit {
        var s: S? = consume self.s
        self.storage?.s.withLock {
            $0 = s.take()!
        }
    }
}
2 Likes

I find the approach is interesting, but I think the reason why Optional.take method works is because it's not consuming but a regular mutating method. So, from compiler's perspective it's certainly fine to call it in closure. On the other hand, although the method doesn't consume the entire value, it consumes a part of itself (the wrapped value), so it does what OP wants to do even if it's just a regular mutating method.

I understand the reason why you use force-unwrapping in your code, but that kind of runtime assert isn't visible to compiler. For example, if you copy Optional.take method code but define it as a consuming method, it wouldn't compile even if using force-unwrapping.

Side note: I think the fact that var s: S? = consume self.s line compiles proves that deinit closure is considered non-escaping, otherwise it shouldn't compile.

…hence “assert at runtime”. Swift doesn’t have a way to declare a closure can only be run once, so ellie’s way is the best we can do today.

1 Like

I agree it's clever workaround. What I wanted to point out was that the code compiled not because of "assert at runtime" but because of Optional.take being not consuming. This might be obvious to other people in the forum, but it puzzled me for a while when I first saw the solution :slight_smile:

2 Likes

Glad you figured it out yourself :grinning_face:. "Turning a consuming operation into a mutating operation on a wrapper" is a typical escape hatch where the language lacks expressivity.

Also, the reason why this line var s: S? = consume self.s is valid, is that there's this rule that we can partially consume a non-copyable aggregate inside its deinit.

3 Likes

Thank you for the workaround and for the explanation!

I didn't think of the problem from the perspective of closure. Than it is actually not a problem of deinit at all...

Though, some question is remaining - shouldn't Mutex.withLock declare the closure as 'one shot' (or 'self consuming closure ') in some way?

1 Like

Yes, ideally, it should. But there's currently no way to do that in Swift.

1 Like