Skip to content

Memory Safety

Swift is designed to prevent unsafe memory access by default. It ensures variables are initialized before use, deallocated memory isn't accessed, and array indices are bounds-checked. Swift also enforces exclusive access to memory during modifications to avoid conflicts. With automatic memory management, you rarely need to worry about memory directly. However, understanding potential conflicts helps you write reliable code. Conflicts trigger compile-time or runtime errors.

What Causes Conflicting Access?

Memory access occurs when you read or write variables, pass arguments, or call functions. For instance:

swift
// Write: Assigning a value.
var count = 5

// Read: Using the value.
print("Current count: \(count)")

Conflicts arise when multiple code parts access the same memory location simultaneously, leading to unpredictable results. This can happen during multi-step modifications, like updating a shared value over several lines.

Imagine updating a shopping list total on paper: You add items first, then recalculate the sum. During the addition, the total is outdated—if you read it mid-process, you get wrong info. Before and after, reads are accurate. Fixing such conflicts requires clarifying the intended final state (e.g., old total vs. new).

Note: These issues occur even on single threads, unlike multithreading (use Thread Sanitizer for that). Swift catches single-thread conflicts at compile or runtime.

Key Traits of Memory Access

Conflicts involve three traits: read/write type, duration, and memory location. A conflict happens if:

  • Accesses aren't both reads or both atomic.
  • They target the same memory spot (e.g., a variable or property).
  • Their durations overlap.

Reads don't change memory; writes do. Duration is instantaneous (quick, no overlap possible) or long-term (spans other code, allowing overlap). Most accesses are instantaneous:

swift
func addBonus(to value: Int) -> Int {
    return value + 2
}

var score = 10
score = addBonus(to: score)
print(score)  // Outputs "12"

Long-term accesses overlap with others and appear in in-out parameters or mutating methods. Atomic accesses (using Atomic or C atomics) are safe from conflicts—see stdatomic(3) for C details.

Conflicts with In-Out Parameters

Functions hold long-term write access to in-out parameters from after non-in-out args are evaluated until the call ends. You can't access the original variable during this, even if allowed otherwise:

swift
var incrementBy = 3

func addTo(_ target: inout Int) {
    target += incrementBy
}

addTo(&incrementBy)  // Error: Conflict on incrementBy

Here, the read of incrementBy overlaps the write to target (same memory). Fix by copying:

swift
var tempIncrement = incrementBy
addTo(&tempIncrement)
incrementBy = tempIncrement  // Now 6

Passing one variable to multiple in-out params also conflicts:

swift
func splitEvenly(_ a: inout Int, _ b: inout Int) {
    let total = a + b
    a = total / 2
    b = total - a
}

var scoreA = 20
var scoreB = 14
splitEvenly(&scoreA, &scoreB)  // OK

splitEvenly(&scoreA, &scoreA)  // Error: Conflict on scoreA

Writes to the same spot overlap. Operators follow the same rules.

Conflicts in Mutating Methods

Mutating methods on structs get write access to self for the full call. Example: A simple inventory tracker.

swift
struct Inventory {
    var itemCount: Int
    var maxItems: Int = 100

    mutating func resetCount() {
        itemCount = maxItems
    }
}

No issue here. But adding an in-out param can conflict:

swift
extension Inventory {
    mutating func transferItems(to other: inout Inventory) {
        splitEvenly(&other.itemCount, &itemCount)
    }
}

var shelfA = Inventory(itemCount: 50)
var shelfB = Inventory(itemCount: 30)
shelfA.transferItems(to: &shelfB)  // OK

Different instances, no conflict. Self-transfer does conflict:

swift
shelfA.transferItems(to: &shelfA)  // Error: Conflict on shelfA

self and the param overlap on the same memory.

Conflicts with Properties

Value types (structs, tuples, enums) treat property/element access as whole-value access. Overlapping writes conflict:

swift
var stats = (points: 15, lives: 3)
splitEvenly(&stats.points, &stats.lives)  // Error: Conflict on stats

Same for struct properties in globals:

swift
var globalItem = Inventory(itemCount: 40)
splitEvenly(&globalItem.itemCount, &globalItem.maxItems)  // Error

Locals often work if safe (no computed props, no escaping closures):

swift
func updateLocal() {
    var localItem = Inventory(itemCount: 40)
    splitEvenly(&localItem.itemCount, &localItem.maxItems)  // OK
}

Swift allows proven-safe overlaps for performance, but enforces exclusivity otherwise.

Released under the MIT License.