Noncopyable Types (~Copyable): Ownership Semantics in Swift


Every value in Swift is copyable by default. You assign a struct to a new variable, pass it to a function, return it from a closure — the compiler silently duplicates it. For most code, that implicit copying is fine. But when you are modeling a resource that must have exactly one owner — a file handle, a database transaction, a GPU buffer — copies are not just wasteful, they are semantically wrong. Swift’s ~Copyable types let you suppress implicit copying at the type level, giving you Rust-style unique ownership without leaving Swift.

This post covers what ~Copyable does, how consuming and borrowing modifiers control ownership transfer, and when you should (and should not) reach for noncopyable types. We will not cover ~Escapable or Span — those are explored in Span and MutableSpan.

Contents

The Problem

Consider a render job in a Pixar-style asset pipeline. Each job holds an exclusive lock on a GPU command buffer. If two parts of your code hold copies of the same job, both might try to commit the buffer, corrupting the frame.

struct RenderJob {
    let sceneID: String
    let commandBufferToken: UInt64

    func commit() {
        print("Committing buffer \(commandBufferToken) for scene '\(sceneID)'")
    }
}

let job = RenderJob(sceneID: "toy-story-4-final", commandBufferToken: 42)
let backup = job  // implicit copy — now two owners
job.commit()
backup.commit()   // double commit — data corruption
Committing buffer 42 for scene 'toy-story-4-final'
Committing buffer 42 for scene 'toy-story-4-final'

The compiler is happy, but the runtime behavior is wrong. You could switch to a class and add manual bookkeeping, but that trades value semantics and stack allocation for heap allocation and reference counting. What you really want is for the compiler to enforce that RenderJob has exactly one owner at all times.

Introducing ~Copyable

The ~Copyable annotation suppresses the implicit Copyable conformance that every Swift type normally receives. Once a type is ~Copyable, the compiler rejects any attempt to copy it.

Note: ~Copyable was introduced in SE-0390 and shipped in Swift 5.9. Noncopyable generics arrived in SE-0427 with Swift 6.0.

struct RenderJob: ~Copyable {
    let sceneID: String
    let commandBufferToken: UInt64

    func commit() {
        print("Committing buffer \(commandBufferToken) for scene '\(sceneID)'")
    }
}

let job = RenderJob(sceneID: "toy-story-4-final", commandBufferToken: 42)
let backup = job  // error: 'job' is borrowed and cannot be consumed
job.commit()

The compiler stops you cold. There is no way to create a second owner of job without explicitly designing your API to allow it. This is the core value proposition: unique ownership enforced at compile time.

What Happens to the Original Binding

When you pass or assign a noncopyable value, ownership transfers. The original binding is consumed and becomes unavailable.

let job = RenderJob(sceneID: "finding-nemo-rerender", commandBufferToken: 7)
let transferred = consume job
// print(job.sceneID)  // error: 'job' used after consume
transferred.commit()
Committing buffer 7 for scene 'finding-nemo-rerender'

The consume keyword makes the ownership transfer explicit. After the consume, job is dead — accessing it is a compile-time error, not a runtime crash.

Ownership Modifiers: consuming, borrowing, and inout

Functions that accept a ~Copyable parameter must declare how they interact with ownership. Swift provides three modifiers:

ModifierOwnership EffectCaller Can Use Value After Call?
borrowingRead-only loan. Caller retains ownership.Yes
consumingOwnership transfers to the callee. Caller loses it.No
inoutExclusive mutable access. Caller keeps ownership.Yes

borrowing — Read Without Taking

Use borrowing when a function needs to inspect a value without claiming it.

struct AssetManifest: ~Copyable {
    let projectName: String
    let assetCount: Int
}

func printSummary(_ manifest: borrowing AssetManifest) {
    print("\(manifest.projectName): \(manifest.assetCount) assets")
}

let manifest = AssetManifest(projectName: "Monsters Inc", assetCount: 2048)
printSummary(manifest)
printSummary(manifest)  // still valid — borrowing did not consume
Monsters Inc: 2048 assets
Monsters Inc: 2048 assets

The caller retains full ownership. You can call printSummary as many times as you want.

consuming — Transfer Ownership

Use consuming when the callee becomes the final owner of the value.

func submitToRenderFarm(_ job: consuming RenderJob) {
    print("Submitting scene '\(job.sceneID)' to render farm")
    job.commit()
    // job is destroyed at end of scope
}

let job = RenderJob(sceneID: "inside-out-2-shot-47", commandBufferToken: 99)
submitToRenderFarm(job)
// job.commit()  // error: 'job' used after being consumed
Submitting scene 'inside-out-2-shot-47' to render farm
Committing buffer 99 for scene 'inside-out-2-shot-47'

After calling submitToRenderFarm, job no longer exists in the caller’s scope. The compiler enforces this statically.

inout — Exclusive Mutable Access

Use inout when you need to mutate the value and return ownership to the caller.

struct LightingPass: ~Copyable {
    var intensity: Double
    let sceneName: String

    mutating func boost(by factor: Double) {
        intensity *= factor
    }
}

func applyDramaticLighting(_ pass: inout LightingPass) {
    pass.boost(by: 2.5)
    print("Boosted '\(pass.sceneName)' to intensity \(pass.intensity)")
}

var pass = LightingPass(intensity: 1.0, sceneName: "Coco Marigold Bridge")
applyDramaticLighting(&pass)
print("Final intensity: \(pass.intensity)")
Boosted 'Coco Marigold Bridge' to intensity 2.5
Final intensity: 2.5

The inout modifier grants the callee exclusive, mutable access. Ownership returns to the caller when the function finishes.

The deinit Story

Noncopyable structs and enums can define a deinit, which is something normally reserved for classes. This is the natural place to release the exclusive resource.

struct DatabaseTransaction: ~Copyable {
    let transactionID: Int
    private var committed = false

    init(id: Int) {
        self.transactionID = id
        print("Transaction \(id): BEGIN")
    }

    mutating func commit() {
        committed = true
        print("Transaction \(transactionID): COMMIT")
    }

    deinit {
        if !committed {
            print("Transaction \(transactionID): ROLLBACK (not committed)")
        }
    }
}

func saveMovieRating() {
    var txn = DatabaseTransaction(id: 1)
    // Simulate saving "Ratatouille" with a 5-star rating
    txn.commit()
    // deinit fires here, but committed == true, so no rollback
}

func failedSave() {
    let _ = DatabaseTransaction(id: 2)
    // Forgot to commit — deinit triggers automatic rollback
}
Transaction 1: BEGIN
Transaction 1: COMMIT
Transaction 2: BEGIN
Transaction 2: ROLLBACK (not committed)

This pattern — automatic cleanup in deinit — is RAII (Resource Acquisition Is Initialization), a technique borrowed from C++ and Rust. The difference from using a class is that you get deterministic destruction with value semantics and no heap allocation.

Tip: When a consuming function receives a noncopyable value, the value’s deinit runs at the end of that function’s scope — unless you explicitly transfer ownership elsewhere with consume.

Noncopyable Generics

Before Swift 6.0, generic code implicitly required Copyable conformance. That meant you could not store a ~Copyable type in a generic container or pass it to a generic function. SE-0427 changed this by letting you suppress the Copyable constraint with the same ~Copyable syntax.

struct UniqueBox<Wrapped: ~Copyable>: ~Copyable {
    private var _value: Wrapped

    init(_ value: consuming Wrapped) {
        _value = value
    }

    consuming func take() -> Wrapped {
        return _value
    }

    borrowing func peek() -> String {
        return String(describing: _value)
    }
}

The key insight: Wrapped: ~Copyable means “Wrapped does not need to be Copyable.” It still can be Copyable — you are relaxing the constraint, not adding a new one.

// Works with noncopyable types
let boxedJob = UniqueBox(RenderJob(sceneID: "wall-e-earth", commandBufferToken: 55))
let job = boxedJob.take()
job.commit()

// Also works with regular Copyable types
let boxedNumber = UniqueBox(42)
let number = boxedNumber.take()
print(number)
Committing buffer 55 for scene 'wall-e-earth'
42

Apple Docs: Copyable — Swift Standard Library

Advanced Usage

Noncopyable Enums for State Machines

Noncopyable enums are excellent for modeling state machines where each state transition consumes the previous state, making illegal transitions unrepresentable.

enum RenderPipelineState: ~Copyable {
    case idle
    case loading(sceneFile: String)
    case rendering(frameCount: Int)
    case complete(outputPath: String)

    consuming func startLoading(file: String) -> RenderPipelineState {
        guard case .idle = self else {
            fatalError("Can only start loading from idle state")
        }
        return .loading(sceneFile: file)
    }

    consuming func beginRender(frames: Int) -> RenderPipelineState {
        guard case .loading = self else {
            fatalError("Can only begin render after loading")
        }
        return .rendering(frameCount: frames)
    }

    consuming func finish(output: String) -> RenderPipelineState {
        guard case .rendering = self else {
            fatalError("Can only finish from rendering state")
        }
        return .complete(outputPath: output)
    }
}

Each transition method is consuming — calling it destroys the old state and produces the new one. You cannot hold onto both .idle and .rendering simultaneously.

var state: RenderPipelineState = .idle
state = state.startLoading(file: "up-house-scene.usd")
state = state.beginRender(frames: 240)
state = state.finish(output: "/renders/up-house-final.exr")

Conditional Copyability

Sometimes a wrapper type should be copyable only when its contents are copyable. Swift supports this with conditional conformance.

struct TaggedValue<T: ~Copyable>: ~Copyable {
    let tag: String
    var value: T
}

extension TaggedValue: Copyable where T: Copyable {}

When T is Int or String, TaggedValue<T> behaves like any other value type with full copy semantics. When T is ~Copyable, the wrapper respects the inner type’s ownership rules. This is the same strategy the standard library uses for Optional and Result in Swift 6.0+.

Warning: A ~Copyable struct that contains a stored property of a copyable type is still noncopyable. The ~Copyable annotation applies to the struct itself — it is not inferred from stored properties.

Performance Considerations

The primary performance benefit of ~Copyable is eliminating unintended copies. For small types (a few words of stack memory), the compiler already optimizes copies away in most cases. The real wins come in two scenarios:

Large value types. If your struct contains a large inline buffer — for example, an InlineArray of pixel data — each copy duplicates the entire buffer. Marking it ~Copyable guarantees zero copies and forces callers to use borrowing or inout access.

Deterministic resource cleanup. By using deinit on a noncopyable struct instead of a class, you avoid heap allocation and reference-counting overhead while still getting deterministic destruction. This matters in tight loops — render passes, audio processing callbacks, networking hot paths — where ARC traffic can show up as measurable overhead in Instruments.

Tip: Profile with Instruments before reaching for ~Copyable as a performance optimization. The compiler’s copy-on-write and move optimizations already eliminate most redundant copies for regular types. Use ~Copyable when you need the semantic guarantee of unique ownership, not just a performance heuristic.

That said, when you combine ~Copyable with InlineArray and stack allocation, you unlock fixed-size, zero-heap data structures that rival C performance — without sacrificing memory safety.

When to Use (and When Not To)

Use ~Copyable when:

  • Exclusive resource ownership — file handles, GPU buffers, DB transactions. The compiler enforces single ownership.
  • State machines with linear transitions — consuming transitions prevent illegal state duplication.
  • Large inline value types on hot paths — eliminates accidental copies. Profile first.

Avoid ~Copyable when:

  • Small, plain data models (Movie, Character, Rating) — the cognitive overhead outweighs any benefit.
  • Types stored in standard Array or Dictionary — standard library collections require Copyable elements (as of Swift 6.0). Use noncopyable generics or custom containers instead.
  • Shared read access from multiple call sites~Copyable enforces exclusive access. Use a class with Sendable or an actor.

Note: Swift’s Optional and Result gained ~Copyable-aware overloads in Swift 6.0. Other standard library types like Array and Dictionary do not yet support noncopyable elements. Track the Swift Evolution dashboard for updates.

Summary

  • ~Copyable suppresses implicit copy semantics, enforcing unique ownership at compile time.
  • Use consuming to transfer ownership, borrowing for read-only loans, and inout for exclusive mutation.
  • Noncopyable structs can define deinit for deterministic cleanup — RAII without the heap.
  • Noncopyable generics (T: ~Copyable) let you write containers and utilities that work with both copyable and noncopyable types.
  • Noncopyable enums model state machines where illegal transitions are unrepresentable.
  • Profile before optimizing. Reach for ~Copyable when you need ownership guarantees, not just performance.

Want to see noncopyable types in action with stack-allocated collections? Head to InlineArray and Stack-Allocated Collections to learn how InlineArray pairs with ~Copyable for zero-heap data structures.