Copy-On-Write in Swift: Semantics, Misconceptions, and a Custom Implementation


Greetings, traveler!

Swift developers encounter copy-on-write (COW) early when working with collections such as Array or String. These types behave like value types while internally sharing storage between copies. The technique allows Swift to preserve value semantics while reducing unnecessary memory duplication.

Understanding how this mechanism works provides practical benefits. It clarifies how Swift collections achieve their performance characteristics, improves reasoning about memory behavior, and enables the design of custom data structures that combine value semantics with efficient storage management.

This article explores the design of copy-on-write storage in Swift, the mental models developers often adopt when reasoning about it, and a practical implementation of a custom COW container.

Value Semantics and Data Isolation

Swift’s type system distinguishes between value semantics and reference semantics. Value types guarantee isolation between instances. Assigning a value or passing it to a function produces an independent value that can evolve without affecting the original.

Consider a simple structure representing a coordinate:

struct Coordinate {
    var latitude: Double
    var longitude: Double
}

var home = Coordinate(latitude: 51.5074, longitude: -0.1278)
var office = home

office.latitude = 40.7128

print(home.latitude)   // 51.5074
print(office.latitude) // 40.7128

Each variable holds its own value. Mutation of one value leaves the other unchanged.

For small structures composed of trivial types such as Int, Bool, or Double, copying typically involves a straightforward memory copy performed by the compiler.

This model scales well for lightweight data. Larger values introduce additional considerations.

The Cost of Copying Large Values

Imagine a structure that represents a dataset loaded into memory:

struct LogArchive {
    var entries: [String]
    var source: String
}

When this value is copied, the entries array and the source string must also be copied according to value semantics. When values contain large buffers or appear frequently in assignments, these copies can become expensive.

Swift’s standard collections address this challenge through storage sharing. Multiple values may temporarily reference the same underlying storage while still presenting value semantics to the outside world.

Storage Sharing as a Design Technique

Copy-on-write relies on separating the public value type from its underlying storage.

The value type acts as the public interface.
A reference type stores the actual data.

Several values may share the same storage object. Once a mutation occurs, the value performing the mutation receives a unique copy of the storage.

Conceptually, the memory layout looks like this:

Value A ──┐
          ├── Shared Storage
Value B ──┘

After mutation:

Value A ─── Storage A
Value B ─── Storage B

This pattern allows inexpensive copying while preserving predictable behavior.

The Uniqueness Check

Swift provides a runtime function that enables this design:

isKnownUniquelyReferenced(_:)

This function determines whether a class instance has exactly one strong reference. When a value type manages storage through a class, this check allows the implementation to decide whether copying becomes necessary before mutation.

The logic typically follows this pattern:

mutating func ensureUniqueStorage() {
    if !isKnownUniquelyReferenced(&storage) {
        storage = Storage(copying: storage)
    }
}

The mutation proceeds only after the storage becomes unique.

Designing a Copy-On-Write Container

A custom copy-on-write type consists of two layers:

  1. A value type providing the public API
  2. A reference type that stores the underlying state

The following example demonstrates a generic container that applies this design.

public struct SharedBuffer<Element> {

    private final class Storage {
        var elements: [Element]

        init(_ elements: [Element]) {
            self.elements = elements
        }

        init(copying other: Storage) {
            self.elements = other.elements
        }
    }

    private var storage: Storage

    public init(_ elements: [Element]) {
        storage = Storage(elements)
    }

    public var elements: [Element] {
        get { 
        		storage.elements 
        }
        set {
            ensureUniqueStorage()
            storage.elements = newValue
        }
    }

    public mutating func append(_ element: Element) {
        ensureUniqueStorage()
        storage.elements.append(element)
    }

    private mutating func ensureUniqueStorage() {
        if !isKnownUniquelyReferenced(&storage) {
            storage = Storage(copying: storage)
        }
    }
}

This design mirrors the pattern used by Swift’s standard collections.

Observing the Behavior

The container behaves like a regular value type from the caller’s perspective.

var archiveA = SharedBuffer(["log1", "log2"])
var archiveB = archiveA

archiveB.append("log3")

print(archiveA.elements)
print(archiveB.elements)

After the assignment, both variables reference the same storage.
When archiveB mutates the buffer, the uniqueness check creates a new storage instance.

Mutation Optimization with _modify

Swift provides accessor modifiers that allow more efficient mutation of stored properties. The _modify accessor exposes mutable storage to the caller while preserving copy-on-write behavior.

An optimized version of the container looks like this:

public struct SharedBuffer<Element> {

    private final class Storage {
        var elements: [Element]

        init(_ elements: [Element]) {
            self.elements = elements
        }

        init(copying other: Storage) {
            self.elements = other.elements
        }
    }

    private var storage: Storage

    public init(_ elements: [Element]) {
        storage = Storage(elements)
    }

    public var elements: [Element] {
        _read {
            yield storage.elements
        }
        _modify {
            ensureUniqueStorage()
            yield &storage.elements
        }
    }

    private mutating func ensureUniqueStorage() {
        if !isKnownUniquelyReferenced(&storage) {
            storage = Storage(copying: storage)
        }
    }
}

This pattern allows in-place mutation while preserving the guarantees of value semantics.

Mental Models That Lead to Confusion

Several simplified explanations often appear in discussions of Swift’s memory model.

A common mental shortcut assumes that structures automatically use copy-on-write. Swift’s language model instead defines value semantics as an observable behavior. Copy-on-write represents an implementation strategy chosen by specific types.

Another misconception equates copy-on-write with deep copying of nested values. Copy-on-write protects the container’s storage. The elements inside the container may still reference shared objects.

Understanding these distinctions helps avoid incorrect assumptions about memory isolation.

When a Custom COW Design Makes Sense

Custom copy-on-write implementations become valuable when certain conditions apply:

  • the structure manages large amounts of data
  • instances are copied frequently
  • mutations occur less often than reads
  • value semantics remain desirable for API design

This combination often appears in collections, buffers, and domain objects that carry large datasets.

For smaller structures composed of a few fields, direct copying typically remains simpler and efficient enough.

Final Thoughts

Copy-on-write provides a practical mechanism for combining value semantics with efficient memory usage. The technique allows multiple values to share storage while preserving the predictable behavior expected from value types. Swift’s standard collections rely heavily on this design, demonstrating how shared storage can coexist with a value-oriented programming model.

A custom implementation follows a straightforward structure: a value wrapper, reference storage, and a uniqueness check before mutation. Once these pieces are understood, the pattern becomes a powerful tool when designing high-performance data structures in Swift.