Is "_ = x" guaranteed to keep a reference to x?

I want to keep an object alive until an async block of code has run. Is the following guaranteed to work?


func test() {
    let x = ObjectToKeepAlive()
    doSomethingAsyncronously(completion: {
       _ = x
    })
}

Or can the optimiser discard the "_ = x" and cause the deinit to happen earlier?

In theory, the reference to "x" is retained throughout the whole scope of the closure. In practice, Apple compilers shorten the scope of a reference to just after the last mention, under the principle that you can't tell the difference if you don't use the reference any more. This ignores the consequences for the lifetime of the referenced object itself.


In your code, the reference object is subject to deallocation after line 4, whether or not the code is optimized. (It might not survive to the end of the closure if you have additional code after line 4.) However, if the code is optimized, line 4 might be discarded, which means "x" might never be captured, and so the object is subject to deallocation after line 2. ( ! )


Unless you retain the object by brute force, there's no trick that will extend its lifetime past the last source code reference in a scope.


I guess the question is, why do you need to keep it alive without using it? The answer to that question will affect the kind (and hackiness) of any solution to the problem you're having.

In this specific case, I'm wrapping an underlying temporary file that gets deleted once the object is freed. Edited for clarity:


class TempFile {
    let url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString, isDirectory: false)
    init(movingFrom: URL) throws {
        try FileManager.default.moveItem(from: movingFrom, to: url)
    }
    deinit {
        try? FileManager.default.removeItem(at: url)
    }
}


func doSomething(url: URL, completion: @escaping ()->()) {
    DispatchQueue.global().async {

       //operate on file at `url` here
        DispatchQueue.main.async(execute: completion)
    }

}

let f = TempFile(movingFrom: somwhere)
doSomething(url: f.url) {
    _ = f

}


The async function I'm passing it into doesn't operate on a TempFile, but a plain file URL. So I need to ensure the TempFile is alive until the completion-block is run and it's safe to delete the file.



It's a bit alarming to hear that that the object lifetimes are not strictly-defined; the idea of control flows changing order so significantly under optimization seems kinda contrary to what I understood Swift to be. RAII is not my favourite thing in the world but there are times when it's handy...

I think you've mistaken "strictly defined" for "deterministic". :-/


The realities of code optimization introduce effective non-determinism in how your code behaves. And the reality of the memory management system that Swift has to be compatible with (Objective-C's) means that object destruction may be locally non-deterministic. Thus, object lifespans are defined as "may end any time after the last reference". And then there's the non-determinism introduced by the asynchronous dispatch...


I think the bigger problem is that you're trying to do something trivial (an assignment to an unused variable) rather that something non-trivial (like an assignment to a property on a trunk object. Or pass to a method that isn't going to be optimized out of existence (like a library method).


In other words, you're going to get silly looks from everyone else if you complain that things like empty for loops, or even

for (x = 0; x < whatever; x++) x;

as the last use of x get optimized to nothing.

Gotcha, thanks.


Where did you get this info btw? The swift book talks about it only in rather vague terms, and I can’t find other references beyond some passing mentions of deterministic destruction from Chris Lattner.

I'd return the book and pocket the refund 😉


Swift.org is your friend, otherwise, I think.


Good luck.

What I would suggest is simply to pass a TempFile into "doSomething" and through as the closure parameter. If there are reasons why you can't do that, I suggest you add an explicit method to TempFile to remove the item at the URL (what deinit now does) and change deinit to invoke it. (Write the new method so it can be called multiple times but only does the remove once.) Then, at the end of your closure (or wherever in your closure you're done with the file, invoke the explicit method … um … explicitly. At that point, you no longer care when the TempFile is deallocated, so whatever the compiler does with the lifetime is fine.


Your real problem is that you're using an anti-pattern: "deallocation as resource management". This almost never does what you want. (It sort of works in a garbage collected environment, so long as you don't do a lot of resource management very quickly, but it's toxic for reference counted environments.) Moving the removeItem out of deinit avoids the pattern and solves your problem.


>> Where did you get this info btw?


It's always been a major issue in Obj-C because of the interaction with interior pointers such as NSData's "bytes" property. Swift makes the use of interior pointers safe (provided you stay away from types with "Unsafe" in their name), but it keeps this same scope-shortening behavior. AFAIK it's not documented anywhere, but it's necessary (say the compiler writers) to make it possible to free up registers early. That's needed because Intel CPUs have relatively few registers, and having to swap pointers to/from memory kills performance.


Note that this optimization gets by "legally" because of a technicality. The rule that the lifetime of a variable is the entire scope in which it declared isn't actually a rule about the semantics of the object pointed to. Technically, the variable stays in scope even though it doesn't exactly exist after the last reference — the only way you'd prove it wasn't in scope would be to reference it, and if you did that the variable lifetime would be extended as far as the reference.


The idea that the variable lifetime controls the object lifetime absolutely is a fiction that we tell ourselves, which the compiler isn't compelled to go along with.

Is "_ = x" guaranteed to keep a reference to x?
 
 
Q