Safer Swift: How ~Copyable Prevents Hidden Bugs

Swift 5.9 introduced the new ~Copyable protocol, which makes an entity "non-copyable." By default, Swift automatically adds the Copyable protocol to all types—without it, value types wouldn't work!

This implicit conformance simplifies our day-to-day work, allowing us to ignore copying mechanics in most cases. However, there are situations where this automatic behavior can lead to subtle bugs in our code.

~Copyable

~Copyable conformance tells the compiler that we do not want our entity to be copied without explicit control. It forces the user to clearly define the owner of the data, ensuring that the compiler enforces strict ownership rules at compile time.

Problem

Consider following code:

struct FileHandleWrapper {
    let handle: UnsafeMutablePointer<FILE>

    init(path: String, mode: String) {
        self.handle = fopen(path, mode)
    }

    func write(_ text: String) {
        fputs(text, handle)
    }
}

Our structure holds a pointer to a file and provides the ability to write to it — simple as that. Now, imagine you’ve created a file handler and want to write something:

func duplicateHandleProblem() {
    let file1 = FileHandleWrapper(path: "test.txt", mode: "w")
    let file2 = file1
    
    file1.write("Hello, ")
    file2.write("world!")
}

The code inside duplicateHandleProblem creates a file handler, copies it to file2, and then writes to what appears to be two separate files. However, as Swift developers, we know that our structure was copied in memory — along with the file pointer. This means both instances are writing to the same file!

One way to prevent this issue is to always remember that FileHandleWrapper is a struct and avoid unintended copies. However, we can take this a step further by letting the compiler enforce this constraint, ensuring such problems never occur.

Fixing problem

We will modify our code by adding ~Copyable:

struct FileHandleWrapper: ~Copyable {
    let handle: UnsafeMutablePointer<FILE>

    init(path: String, mode: String) {
        guard let file = fopen(path, mode) else {
            fatalError("Failed to open file")
        }
        self.handle = file
    }

    func write(_ text: String) {
        fputs(text, handle)
    }

    deinit {
        print("Closing file handle")
        fclose(handle)
    }
}

Our struct now conforms to ~Copyable. As a result, the duplicateHandleProblem function will no longer compile! Instead, we’ll encounter a new error: 'file1' used after consume'.

This happens because assigning file1 to file2 "consumes" file1, transferring ownership. Since file1 no longer holds its underlying value, attempting to use it afterward results in a compilation error.

Additional bonus

Since the FileHandleWrapper struct now has a closely monitored lifecycle, with a well-defined point of memory deallocation, we can implement a deinit method. This allows us to conveniently close the file when the instance is no longer needed.

New keywords

Swift 5.9 adds new keywords, that can be used with ~Copyable protocol:

  1.  borrow – Temporary Read-Only Access
    The borrow modifier allows you to temporarily read a move-only value without taking ownership. The original value remains usable after the function call.
  2. consume – Take Full Ownership and Invalidate the Original
    The consume modifier moves ownership of the value into a function, meaning the original reference becomes invalidafter the call.
  3. inout – Temporary Mutable Access Without Moving Ownership
    The inout modifier allows a function to temporarily modify a value but not take ownership. The original remains usable after the function.
Modifier Ownership Taken? Can Modify? Value Usable After? Use Case
borrow ❌ No ❌ No ✅ Yes Read-only access
consume ✅ Yes ✅ Yes ❌ No Taking full ownership
inout ❌ No ✅ Yes ✅ Yes Mutating without moving

I hope you learned something new! Thank you for reading!