Almost Manual ARC in Swift. Ownership Modifiers and Lifetime Guarantees

Almost Manual ARC in Swift. Ownership Modifiers and Lifetime Guarantees

Swift ownership modifiers and lifetime guarantees

or

Mastering withExtendedLifetime and Parameter Ownership Modifiers (borrowing and consuming) in Swift

Let’s be honest — in the age of clickbait titles, you’ve got to do what you’ve got to do. “Almost Manual ARC in Swift: Ownership Modifiers and Lifetime Guarantees” sounds like we’re unlocking a secret Apple has been hiding since 2011. But hey, if it gets you to scroll — mission accomplished.😄

Swift continues to evolve, introducing features that enhance memory management, performance, and code clarity. Among these are withExtendedLifetime and parameter ownership modifiers (borrowing and consuming). These tools provide developers with fine-grained control over object lifetimes and ownership semantics, enabling safer and more efficient code. In this article, we’ll explore both the simplified and full signatures of withExtendedLifetime, as well as the practical use of borrowing and consuming, with examples and explanations.


Understanding withExtendedLifetime

The withExtendedLifetime function ensures that a given instance is not deallocated before the execution of a closure completes. This is particularly useful when synchronous work depends on a resource staying alive, such as a file or network connection. It does not wait for asynchronous work scheduled inside the closure.

Simplified Signature (Pseudo-code for Demonstration)

func withExtendedLifetime<T>(_ x: T, _ body: () -> Void)

This simplified version is often used to demonstrate the concept. It takes an object x and a closure body, ensuring that x remains alive until body finishes executing.

Full Signature (Real Implementation)

Here is a Swift 6 form of the withExtendedLifetime signature from the standard library:

public func withExtendedLifetime<T, E, Result>(
    _ x: borrowing T, 
    _ body: () throws(E) -> Result
) throws(E) -> Result 
where E : Error, T : ~Copyable, Result : ~Copyable

And its variant with a parameter passed to the closure:

public func withExtendedLifetime<T, E, Result>(
    _ x: borrowing T, 
    _ body: (borrowing T) throws(E) -> Result
) throws(E) -> Result 
where E : Error, T : ~Copyable, Result : ~Copyable

Key Details:

  • borrowing T : Indicates that the function borrows the value without taking ownership.
  • throws(E) : Allows the function to throw errors of type E.
  • Result : The return type of the closure, which is also returned by withExtendedLifetime.
  • T : ~Copyable : Suppresses the implicit Copyable requirement on T, so the function can accept both copyable and noncopyable types. It does not require the type to be noncopyable.

For simplicity, we’ll use the pseudo-code version in examples, but the full signature demonstrates the power and flexibility of withExtendedLifetime.

Practical Example: Using withExtendedLifetime

Let’s consider a scenario where we create a temporary resource and use it in an asynchronous task. A weak capture does not keep it alive, so it may be deallocated before the task uses it.

import Foundation
import Combine

final class Resource: Sendable {
    let name: String
    
    init(name: String) {
        self.name = name
        print("Resource '\(name)' created")
    }
    
    deinit {
        print("❌ Resource '\(name)' deallocated")
    }
    
    func use() {
        print("🎯 Using resource '\(name)'")
    }
}

func createResource() -> Resource {
    return Resource(name: "TemporaryResource")
}

func exampleWithoutExtendedLifetime() {
    // Create a temporary resource
    let resource = createResource()
    
    // Use the resource outside the block
    DispatchQueue.global().async { [weak resource] in
        print("Starting work with the resource")
        resource?.use()
        print("Finishing work with the resource")
    }
    
    print("Block completed")
}

exampleWithoutExtendedLifetime()

One Possible Output Without withExtendedLifetime:

Resource 'TemporaryResource' created
Block completed
❌ Resource 'TemporaryResource' deallocated
Starting work with the resource
Finishing work with the resource

In this possible execution, the resource is deallocated before the asynchronous task uses it, so 🎯 Using resource 'TemporaryResource' is missing. The task’s timing is not guaranteed; a different run may use the resource before it is deallocated.

Now, let’s combine a strong capture with withExtendedLifetime:

func exampleWithExtendedLifetime() {
    // Create a temporary resource
    let resource = createResource()
    
    // Keep the resource alive while the queued work waits to run
    DispatchQueue.global().async { [resource] in
        // Extend its lifetime through the synchronous work below
        withExtendedLifetime(resource) {
            print("Starting work with the resource")
            resource.use()
            print("Finishing work with the resource")
        }
    }
    
    print("Block completed")
}

exampleWithExtendedLifetime()

One Possible Output With withExtendedLifetime:

Resource 'TemporaryResource' created
Block completed
Starting work with the resource
🎯 Using resource 'TemporaryResource'
Finishing work with the resource
❌ Resource 'TemporaryResource' deallocated

The strong capture keeps the resource alive until the queued closure runs. Inside that closure, withExtendedLifetime guarantees it remains alive through all the synchronous work, including the final log message. Wrapping only the call that schedules the task would not provide this guarantee. The “Block completed” message can appear at a different point because scheduling and execution happen concurrently.

Understanding borrowing and consuming in Swift Ownership Model

Swift is evolving its ownership system to improve safety and performance. Two important concepts in this area are borrowing and consuming parameters. In this article, we’ll explain what they mean, how to use them, and what you need to know to experiment with these features today.

What Are borrowing and consuming?

  • borrowing means temporarily accessing a value without taking ownership. The caller keeps it alive for the call. This can avoid an ownership-related retain, but it does not guarantee that the function body performs no ARC operations or copies.

  • consuming means taking ownership of a value. A noncopyable argument cannot be reused after it is consumed. For a copyable argument, Swift may pass a copy or retained reference, allowing the caller to use its original value again.

Together, these modifiers help write safer and more efficient Swift code, especially when working with resources that should not be duplicated or misused.

Swift Version Requirement

These ownership modifiers are available in stable Swift 5.9 and later. The full signatures shown above also use typed throws and noncopyable generics, which were introduced in Swift 6.0. You can check the latest supported Swift versions and features at the official Swift website: swift.org/download; and of course, you can check Swift version on your machine via swift --version terminal command.

Already in Use in the Swift Standard Library

borrowing and consuming are stable language features and are also used in Swift’s Standard Library.

For example, the signature of withExtendedLifetime uses this form in Swift 6 toolchains (the generic constraints are omitted here):

public func withExtendedLifetime<T, E, Result>(
  _ x: borrowing T,
  _ body: (borrowing T) throws(E) -> Result
) throws(E) -> Result

This shows how borrowing is used inside the standard library to avoid unnecessary ARC operations while the function guarantees the value’s lifetime. You can also use borrowing and consuming in your own function signatures without experimental flags.

How to Use Them Yourself

To write your own functions with borrowing or consuming modifiers:

  1. Use a stable Swift 5.9 or later toolchain for the parameter modifiers and the noncopyable Person example below.
  2. Use Swift 6.0 or later for the typed throws and noncopyable generic signatures shown above.

Earlier development toolchains used this flag for noncopyable generics:

-enable-experimental-feature NoncopyableGenerics

This flag is not required for these features in stable Swift 6.0 and later. It was a development flag for noncopyable generics, not a requirement for the borrowing and consuming parameter modifiers.

For more detailed info about these features, see this discussion by Swift contributors:

Try It Yourself: Swift Package Playground

To help you experiment easily, I created a Swift Package Manager (SPM) project with the older flag configuration shown below. The flag is unnecessary with a stable Swift 6.0 or later toolchain. You can clone it from my GitHub repository

This project is set up as a lightweight Playground-style SPM app with this flag configuration:

swiftSettings: [
  .unsafeFlags(["-enable-experimental-feature", "NoncopyableGenerics"])
]

Example Code and Console Output

struct Person: ~Copyable {
    let name: String
}

// borrowing: use the value without taking ownership
func printPersonName(_ person: borrowing Person) {
    print("Hello, \(person.name)")
}

// consuming: we take the object, it can no longer be used afterwards
func sayGoodbye(_ person: consuming Person) {
    print("Goodbye, \(person.name)")
}

func test() {
    let person = Person(name: "John Doe")

    printPersonName(person)   // ✅ person remains available
    sayGoodbye(person)        // ✅ object is now considered "consumed"
    
    printPersonName(person)   // ❌ error: person has already been used
}

test()

Compiler Diagnostic:

This example intentionally fails to compile. The diagnostic wording can vary by compiler version:

error: 'person' used after consume

Remove the final printPersonName call after sayGoodbye to run the valid path. Its console output is:

Hello, John Doe
Goodbye, John Doe

Attempting to use person after sayGoodbye causes a compile-time error ('person' used after consume), preventing unsafe reuse of moved data.

Summary

borrowing and consuming are powerful modifiers in Swift’s evolving ownership model. They help manage memory safety by controlling how values are accessed and transferred. The ownership modifiers and the noncopyable Person example require Swift 5.9 or later. The typed throws and noncopyable generic signatures shown here require Swift 6.0 or later; no experimental flags are needed.

Feel free to reach out if you want me to add more examples or help set this up!

Additional Resources

https://github.com/nikolay-dementiev/OwnershipDemo_Mastering_with_extended_lifetime_and_parameter_ownership_modifiers

https://developer.apple.com/documentation/swift/withextendedlifetime%28_%3A_%3A%29-59dz3

https://github.com/swiftlang/swift-evolution/blob/main/proposals/0377-parameter-ownership-modifiers.md

https://github.com/swiftlang/swift-evolution/blob/main/proposals/0413-typed-throws.md

https://github.com/swiftlang/swift-evolution/blob/main/proposals/0427-noncopyable-generics.md

Last updated on

On this page