Swift

Swift Value Type Copy-on-Write Mechanism

By Seren  |  19 Sep, 2026  |  Leave a comment


I did not learn copy-on-write from an interview question. I learned it while profiling a document editor that kept a published version and an editable draft in memory at the same time. Each document contained thousands of text lines. I expected creating the draft to duplicate all of that storage immediately, but the assignment was cheap. The visible cost arrived only after the editor changed the draft.

That behavior was useful, but my first explanation of it was wrong. I printed the address returned by withUnsafePointer(to: &array) and treated it as the address of the array’s element buffer. It was actually the address of the local Array value. Two different variables naturally have different locations even when their internal storage is shared. To make the behavior testable, I built a small value type with explicit reference storage, a storage identity, and a clone counter.

The Draft Editor That Made Copying Visible

The editor loaded release notes from disk and kept the last published document available for comparison. When a writer clicked Edit, the application assigned that document to a new workingCopy variable. Most sessions changed only a few lines, so copying every string and array during that first assignment would have been wasteful.

At the same time, the two documents had to behave like independent values. Appending a line to the draft must never change the published version. This is the problem copy-on-write solves: values can temporarily share backing storage while they are only being read, then separate before one of them mutates that storage.

The optimization is an implementation technique, not a change to value semantics. From the caller’s point of view, assigning and then modifying a struct should still leave the original value unchanged. If the original changes too, the custom type has reference semantics leaking through its value-type interface.

The Small Storage Class Behind the Struct

The sample defines DraftDocument as a struct. Instead of storing its title and line array directly, it stores one private DocumentStorage reference. The storage is a class, so assigning the outer struct initially copies only that reference. Both values can read the same title and lines without changing either one.

private final class DocumentStorage {
    static var cloneCount = 0
    var title: String
    var lines: [String]

    func cloned() -> DocumentStorage {
        Self.cloneCount += 1
        return DocumentStorage(title: title, lines: lines)
    }
}

The clone counter is there for the demo, not as a production metric. It gives the test a concrete result: assignment should not increment it, the first mutation of shared storage should increment it once, and a later mutation of storage that is already unique should not increment it again.

Checking Uniqueness Before Every Mutation

Every operation that can change the reference storage goes through one method. Swift’s isKnownUniquelyReferenced receives the storage reference as inout. If Swift can confirm that this is the only strong reference, the mutation can proceed in place. Otherwise, the document creates its own storage first.

private mutating func makeUniqueStorage() {
    if !isKnownUniquelyReferenced(&storage) {
        storage = storage.cloned()
    }
}

mutating func append(_ line: String) {
    makeUniqueStorage()
    storage.lines.append(line)
}

The order matters. Mutating the storage and checking afterward would already have changed every value that shared it. I keep the uniqueness check immediately beside the mutation rather than relying on each caller to remember it. If the type later gains rename, removeLine, or replaceAll, each of those paths must establish unique storage before writing.

The function is intentionally conservative: a false result means the reference is not known to be unique. That is enough reason to copy before mutation. I do not use it as a general-purpose reference-count inspector or build business behavior around an assumed numeric retain count.

How the Demo Proves Storage Sharing

Because this is our own storage class, the demo can expose an ObjectIdentifier for testing. The published document and working copy have the same storage identifier immediately after assignment. Once the working copy appends a line, their identifiers differ and the clone count is one.

The more important assertion checks behavior: the published document still contains two lines while the draft contains three. Identity explains how the optimization works, but independent values are the contract users of the type care about. A test that checks only addresses can miss a semantic bug.

The next test renames the draft. At that point nobody else owns its storage, so the clone count stays unchanged. This is the part that made the pattern worthwhile in the editor. After the first change separates the draft, a normal editing session can perform many additional mutations without cloning the whole document on every keystroke.

Why Unsafe Array Addresses Misled Me

withUnsafePointer(to: &array) provides temporary access to the Array value stored in that variable. It does not promise to reveal the identity of the collection’s backing allocation. Printing that pointer for two variables is therefore not a valid CoW experiment.

Even examining an element pointer needs care. Empty arrays have no first element, contiguous storage is exposed only within a closure, and an implementation may change without preserving an address across mutations. Optimizers also have freedom that makes address-based demonstrations fragile. I now test public value behavior for standard collections and use explicit identity only in a custom type whose storage I control.

This also avoids another overstatement: assignment of every Swift Array is not guaranteed to be a universal zero-cost operation, and every first mutation is not automatically an O(n) copy. Capacity, element representation, bridging, storage form, optimization, and the exact operation all matter. CoW often makes sharing efficient, but performance claims still need measurement in the actual workload.

Nested Value Types Can Hide Another Copy

The sample storage class contains a Swift Array<String>. Cloning DocumentStorage assigns that array into the new storage object. The array may continue sharing its own backing storage until the new document appends a line. In other words, a custom CoW layer can sit on top of a standard-library CoW collection.

This layered behavior is useful, but it makes casual byte-count claims unreliable. The storage object has one identity, while its title and line collection may have their own optimized representations. In production I profile the editing operation and memory peak rather than trying to infer the entire ownership graph from one printed address.

Reference-typed elements are another boundary. Copying an array of class instances gives the array value semantics for its sequence of references; it does not deep-copy every referenced object. If two document values contain the same mutable reference element, changing that element can still be visible through both documents. Deep value semantics require either value-typed elements or an explicit element-copying policy.

Copy-on-Write Does Not Make Shared Mutation Thread-Safe

CoW prevents one ordinary value mutation from unexpectedly changing another value. It does not authorize simultaneous unsynchronized access to the same variable. If two tasks read and write shared mutable state at the same time, the program can still have a data race.

In the editor, one actor owned the working document. Background export received a snapshot value and returned a result rather than reaching back into the live draft. That ownership rule was easier to reason about than trying to make the storage class internally lock every operation. Use actors, a serial queue, or another deliberate synchronization design when state crosses concurrency boundaries.

Run the Complete Project

The repository is a standalone Swift command-line project for macOS. It does not require an iOS simulator, CocoaPods, or a package dependency. After installing the Xcode Command Line Tools, run:

git clone https://github.com/2252408699/swift-copy-on-write-document-demo.git
cd swift-copy-on-write-document-demo
make run

A successful run prints five PASS lines, reports one storage clone, and ends with All copy-on-write checks passed. The build treats warnings as errors. Run make clean when you want to remove the executable.

What I Check in a Custom CoW Type

  • All write paths call the uniqueness guard before touching reference storage.
  • The copied value remains unchanged after another copy mutates.
  • Storage cloning includes every field needed for an independent value.
  • Reference-typed elements have an intentional sharing or deep-copy policy.
  • Tests verify behavior as well as storage identity.
  • Concurrency ownership is explicit instead of being delegated to CoW.

For the document editor, this design kept snapshots cheap when they were only read and kept edits independent when they changed. The useful result was not a clever pointer printout. It was a small type whose semantics were obvious at the call site and whose copying behavior could be proved by a test.

Complete source code: swift-copy-on-write-document-demo on GitHub

Seren
Seren

A developer exploring iOS, Objective-C, Swift, and other tech stacks. Here to document my learning process, pitfalls, and growing journey across new technologies.

Your email address will not be published. Required fields are marked *