Copyable and ~Copyable in Swift: Ownership Without the Overhead
Swift has always made copies cheap to reason about: value types copy on assignment, reference types share implicitly. But sometimes you don’t want a value to be copyable at all — you want one owner, and you want the compiler to enforce it.
That’s what ~Copyable is for.
What Is Copyable?
In Swift, every type implicitly conforms to Copyable. This is a protocol-like constraint that says “this type can be copied.” Structs, enums, classes — they all conform. Before Swift 5.9, you couldn’t opt out of this.
~Copyable is the suppression syntax: ~Copyable means “this type does not conform to Copyable.” The compiler then enforces that the value can only ever be moved — transferred from one owner to another — never duplicated.
struct FileHandle: ~Copyable {
private let fd: Int32
init(path: String) throws {
self.fd = open(path, O_RDONLY)
guard fd >= 0 else { throw POSIXError(.ENOENT) }
}
deinit {
close(fd)
}
}
Try to copy this and the compiler refuses:
let handle = try FileHandle(path: "/etc/hosts")
let copy = handle // ❌ error: 'handle' is a noncopyable type
Consuming and Borrowing
Two new ownership modifiers become important with noncopyable types.
consuming
A consuming parameter or method takes ownership of the value. After the call, the original is gone — you can’t use it:
func process(_ handle: consuming FileHandle) {
// handle is owned here
// After this function returns, handle is destroyed
}
let handle = try FileHandle(path: "/etc/hosts")
process(handle)
// Using handle here is a compile error — it was consumed
borrowing
A borrowing parameter borrows the value for the duration of the call without taking ownership:
func inspect(_ handle: borrowing FileHandle) {
// Read-only access. handle is still alive after this returns.
}
let handle = try FileHandle(path: "/etc/hosts")
inspect(handle)
inspect(handle) // ✅ fine — handle was never consumed
mutating on noncopyable types
You can still mutate noncopyable structs with mutating methods:
struct Buffer: ~Copyable {
private var bytes: [UInt8]
init(capacity: Int) {
self.bytes = Array(repeating: 0, count: capacity)
}
mutating func write(_ data: [UInt8], at offset: Int) {
bytes.replaceSubrange(offset..<offset + data.count, with: data)
}
borrowing func read(at offset: Int, count: Int) -> [UInt8] {
Array(bytes[offset..<offset + count])
}
}
Real Use Case 1: File Descriptor Management
This is the textbook case — and for good reason. File descriptors are OS resources. You open one, use it, close it. If you accidentally create two owners that both close it, you get bugs that are silent and hard to reproduce.
struct File: ~Copyable {
private let descriptor: Int32
private let path: String
init(path: String, flags: Int32 = O_RDONLY) throws {
let fd = open(path, flags)
guard fd >= 0 else {
throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EPERM)
}
self.descriptor = fd
self.path = path
}
borrowing func readLine() -> String? {
// read from descriptor
var line = ""
var char = [UInt8](repeating: 0, count: 1)
while read(descriptor, &char, 1) > 0 {
guard char[0] != UInt8(ascii: "\n") else { break }
line.append(Character(UnicodeScalar(char[0])))
}
return line.isEmpty ? nil : line
}
consuming func close() {
Darwin.close(descriptor)
discard self // Prevent deinit from closing again
}
deinit {
Darwin.close(descriptor)
}
}
Note discard self in the consuming close() — it tells the compiler to skip the deinit, avoiding a double-close. This is only valid inside a consuming method.
Usage:
func processLog(at path: String) throws {
var file = try File(path: path)
while let line = file.readLine() {
print(line)
}
// file.close() // optional explicit close
// deinit closes automatically at end of scope
}
Real Use Case 2: Lock Guard
A lock that can be acquired but not accidentally passed around is a classic ownership problem. In C++ you’d use std::unique_lock. In Swift with ~Copyable:
final class Mutex: Sendable {
private let lock = NSLock()
func acquire() -> Guard {
lock.lock()
return Guard(mutex: self)
}
fileprivate func release() {
lock.unlock()
}
struct Guard: ~Copyable {
private let mutex: Mutex
fileprivate init(mutex: Mutex) {
self.mutex = mutex
}
deinit {
mutex.release()
}
}
}
The Guard cannot be copied. It lives exactly as long as the scope it’s created in, then releases the lock:
let mutex = Mutex()
func criticalSection() {
let guard_ = mutex.acquire()
// Lock is held here
// ... do work ...
// guard_ goes out of scope → deinit → lock released
}
// Attempting to copy the guard won't compile:
// let copy = guard_ // ❌
Real Use Case 3: Move-Only Token
Authentication tokens, transaction IDs, one-time-use codes — values that should be consumed exactly once. ~Copyable makes this a compile-time guarantee instead of a runtime assertion:
struct OTPToken: ~Copyable {
let code: String
let expiresAt: Date
var isExpired: Bool { Date() > expiresAt }
consuming func verify(against input: String) -> Bool {
guard !isExpired else { return false }
return code == input
// token is consumed after this call — can't be verified twice
}
}
func authenticate(token: consuming OTPToken, input: String) throws {
guard token.verify(against: input) else {
throw AuthError.invalidToken
}
// ✅ Token is consumed — can never be reused
}
Without ~Copyable, preventing double-use requires a runtime flag (var isUsed = false) and discipline. With it, the compiler handles enforcement for free.
Real Use Case 4: Unique Buffer / Memory Region
When wrapping unsafe memory regions, two owners pointing to the same memory and both deallocating it is a memory corruption bug. ~Copyable prevents this structurally:
struct ManagedBuffer: ~Copyable {
private let pointer: UnsafeMutableRawPointer
private let byteCount: Int
init(byteCount: Int) {
self.pointer = UnsafeMutableRawPointer.allocate(
byteCount: byteCount,
alignment: MemoryLayout<UInt8>.alignment
)
self.byteCount = byteCount
}
mutating func write<T>(_ value: T, at offset: Int) {
pointer.storeBytes(of: value, toByteOffset: offset, as: T.self)
}
borrowing func read<T>(as type: T.Type, at offset: Int) -> T {
pointer.loadUnaligned(fromByteOffset: offset, as: T.self)
}
deinit {
pointer.deallocate()
}
}
Ownership transfer is explicit:
func fillBuffer() -> ManagedBuffer {
var buffer = ManagedBuffer(byteCount: 1024)
buffer.write(UInt32(42), at: 0)
return buffer // ownership transferred to caller
}
var buf = fillBuffer()
let value: UInt32 = buf.read(as: UInt32.self, at: 0)
// buf.deallocate() — happens automatically in deinit
Generics and ~Copyable
Generics in Swift default to requiring Copyable. To write a generic function that accepts noncopyable types, you must explicitly relax the constraint:
// This works only with Copyable types:
func duplicate<T>(_ value: T) -> (T, T) {
(value, value)
}
// This accepts both Copyable and ~Copyable:
func consume<T: ~Copyable>(_ value: consuming T) {
// value is consumed here
}
Protocols can also be noncopyable:
protocol Resource: ~Copyable {
borrowing func use()
consuming func release()
}
What ~Copyable Doesn’t Do
A few things worth being clear about:
It’s not Sendable. Noncopyable types don’t automatically gain Sendable conformance. If you want thread-safety guarantees, you still need to handle that separately.
It doesn’t prevent moves across actor boundaries. Sending a noncopyable value between actors is allowed if the type is Sendable.
It’s not the same as final. Noncopyable types can still be inherited from (for classes) or extended.
Classes are reference types already. The most compelling use cases for ~Copyable are structs and enums. Classes already share a single instance — the problem they solve is different.
Real Use Case 5: Compiler-Enforced State Machine
State machines are one of the most underrated applications of ~Copyable. The core idea: each state is a noncopyable value. A transition consumes the current state and returns a new one. The compiler then makes it impossible to be in two states simultaneously or to perform an invalid transition.
Consider a payment flow with four states: Idle, Processing, Completed, Failed. Without ownership, nothing stops you from calling complete() twice or keeping a stale reference to an old state. With ~Copyable, the compiler enforces the flow structurally.
Defining the states
Each state is a noncopyable struct. It carries only the data relevant to that phase:
struct PaymentIdle: ~Copyable {
let amount: Decimal
let currency: String
}
struct PaymentProcessing: ~Copyable {
let amount: Decimal
let currency: String
let transactionID: String
let startedAt: Date
}
struct PaymentCompleted: ~Copyable {
let transactionID: String
let amount: Decimal
let completedAt: Date
let receiptURL: URL
}
struct PaymentFailed: ~Copyable {
let transactionID: String?
let reason: String
let failedAt: Date
let isRetryable: Bool
}
Transitions as consuming functions
Each transition is a free function (or method) that takes a consuming parameter and returns the next state. Consuming the input means you physically cannot use the old state after the transition:
// Idle → Processing: starts the transaction
func startPayment(
_ state: consuming PaymentIdle,
transactionID: String
) -> PaymentProcessing {
PaymentProcessing(
amount: state.amount,
currency: state.currency,
transactionID: transactionID,
startedAt: Date()
)
}
// Processing → Completed
func completePayment(
_ state: consuming PaymentProcessing,
receiptURL: URL
) -> PaymentCompleted {
PaymentCompleted(
transactionID: state.transactionID,
amount: state.amount,
completedAt: Date(),
receiptURL: receiptURL
)
}
// Processing → Failed
func failPayment(
_ state: consuming PaymentProcessing,
reason: String,
isRetryable: Bool = false
) -> PaymentFailed {
PaymentFailed(
transactionID: state.transactionID,
reason: reason,
failedAt: Date(),
isRetryable: isRetryable
)
}
// Failed → Processing: retry (only if retryable)
func retryPayment(
_ state: consuming PaymentFailed,
newTransactionID: String
) -> PaymentProcessing? {
guard state.isRetryable else { return nil }
// We can't access state.amount here — it was consumed
// The state machine would need to carry original data if needed
return PaymentProcessing(
amount: 0, // in practice, stored in PaymentFailed or passed separately
currency: "EUR",
transactionID: newTransactionID,
startedAt: Date()
)
}
What the compiler prevents
var idle = PaymentIdle(amount: 9.99, currency: "EUR")
var processing = startPayment(idle, transactionID: "txn_001")
// ❌ Cannot use idle after it was consumed
// print(idle.amount)
// ❌ Cannot start payment again from the same idle state
// let processing2 = startPayment(idle, transactionID: "txn_002")
// ❌ Cannot complete the same processing state twice
// let completed1 = completePayment(processing, receiptURL: url)
// let completed2 = completePayment(processing, receiptURL: url)
// ✅ Only one valid transition from processing
let completed = completePayment(processing, receiptURL: URL(string: "https://receipts.com/txn_001")!)
All three errors above are caught at compile time — not at runtime, not in tests, not in production. The invalid code simply doesn’t build.
Using it in an async context
In practice, transitions happen in async functions. The pattern still holds:
@MainActor
final class PaymentCoordinator {
private var state: PaymentState
// PaymentState is an enum that wraps the noncopyable states
// (using existential or enum-based wrapping — see note below)
}
One practical note: enums themselves can be ~Copyable in Swift 5.9+, which lets you wrap all states in a single type:
enum PaymentState: ~Copyable {
case idle(PaymentIdle)
case processing(PaymentProcessing)
case completed(PaymentCompleted)
case failed(PaymentFailed)
}
And the coordinator manages transitions:
@MainActor
final class PaymentCoordinator {
private var state: PaymentState
init(amount: Decimal, currency: String) {
self.state = .idle(PaymentIdle(amount: amount, currency: currency))
}
func start() async {
guard case .idle(let idle) = consume state else { return }
let transactionID = UUID().uuidString
state = .processing(startPayment(idle, transactionID: transactionID))
await processWithAPI()
}
private func processWithAPI() async {
guard case .processing(let processing) = consume state else { return }
do {
let receipt = try await PaymentAPI.charge(transactionID: processing.transactionID)
state = .completed(completePayment(processing, receiptURL: receipt.url))
} catch {
state = .failed(failPayment(processing, reason: error.localizedDescription, isRetryable: true))
}
}
}
consume state extracts the value and moves it out of self.state. The compiler knows that between the consume and the reassignment, state is uninitialized — any path that doesn’t reassign it is a compile error. This forces you to handle all transitions explicitly, making incomplete implementations impossible to ship.
Compared to the classic approach
Without ~Copyable, the typical state machine uses an enum with associated values and a switch. It works, but nothing prevents you from keeping a stale reference, calling a method on the wrong state, or forgetting to transition after an async operation. The ~Copyable version moves all of those checks to the compiler.
When to Use It
~Copyable is not a performance feature in the general case — for most app-level code, the value type / reference type distinction already gives you what you need. It shines in specific situations:
- Resource wrappers: file descriptors, sockets, GPU buffers, OS handles — anything that must be closed/released exactly once
- One-time-use semantics: tokens, nonces, single-fire callbacks
- Low-level memory management: unsafe pointer wrappers where double-free must be structurally impossible
- Lock guards: ensuring locks are released exactly when they leave scope
If you’re writing app-level UI code, you probably don’t need it. If you’re writing a networking library, a database abstraction, or anything that wraps OS resources — ~Copyable is worth knowing.