Almost Manual ARC in Swift. Ownership Modifiers and Lifetime Guarantees

Almost Manual ARC in 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 for managing resources like files, network connections, or asynchronous tasks where premature deallocation could lead to runtime errors.

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’s the actual signature of withExtendedLifetime from Swift’s 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 : Ensures that the type T is not trivially copyable, aligning with Swift’s ownership model.

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. Without withExtendedLifetime, the resource might be deallocated prematurely.

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")
}

exampleWithExtendedLifetime()

Output Without withExtendedLifetime:

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

Here, the resource is deallocated before the asynchronous task completes, leading to potential issue (As result we can’t find 🎯 Using resource 'TemporaryResource' there).

Now, let’s fix this using withExtendedLifetime:

func exampleWithExtendedLifetime() {
    // Create a temporary resource
    let resource = createResource()
    
    // Extend the lifetime of the resource
    withExtendedLifetime(resource) {
        DispatchQueue.global().async { [weak resource] in
            print("Starting work with the resource")
            resource?.use()
            print("Finishing work with the resource")
        }
    }
    
    print("Block completed")
}

exampleWithExtendedLifetime()

Output With withExtendedLifetime:

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

In this case, the resource remains alive until the asynchronous task completes, ensuring safe usage.

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 increasing its reference count (no ARC retain). It’s a lightweight way to read data without affecting the lifetime of the object.

  • consuming means taking ownership of a value — you “consume” it, so it can no longer be used after the call. This prevents accidental reuse and enforces safer memory management.

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 starting from Swift 5.9+ and are actively being developed for future Swift versions. You can check the latest supported Swift versions and features at the official Swift website: https://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

Although borrowing and consuming are still experimental language features, they are already used in Swift’s Standard Library.

For example, the signature of withExtendedLifetime is defined internally like this (as of Swift 5.9+ toolchains):

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

This shows how borrowing is leveraged inside the standard library to avoid unnecessary ARC operations and to make guarantees about lifetimes. However, you can’t yet use borrowing or consuming in your own function signatures unless you opt into experimental language features.

How to Use Them Yourself (Experimental)

To write your own functions with borrowing or consuming modifiers, you must:

  1. Use Swift 5.9 or later with nightly or development toolchains.
  2. Explicitly enable experimental features using the compiler flag:
-enable-experimental-feature NoncopyableGenerics

This means they are not yet part of stable Swift releases and require nightly or development toolchains.

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 these flags enabled. You can clone it from my GitHub repository

This project is set up as a lightweight Playground-style SPM app with the required flags in place:

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

Example Code and Console Output

struct Person: ~Copyable {
    let name: String
}

// borrowing: read-only, no ARC retain
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)   // ✅ no ARC retain
    sayGoodbye(person)        // ✅ object is now considered "consumed"
    
    printPersonName(person)   // ❌ error: person has already been used
}

test()

Console Output:

Hello, John Doe
Hello, 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. Currently, these features require Swift 5.9+ and enabling experimental flags.

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

Last updated on