If you’ve spent any time on Medium or watched a few YouTube tutorials, or if (god forbid) you’ve asked ChatGPT for help, you’ve probably heard some version of this:
“Structs in Swift use Copy-On-Write (COW).”
What’s copy-on-write? That means that under the hood, Swift structs automatically share storage between copies… right up until the point where until you mutate one of them. Then — and only then — does SwiftUI copy the data in question.
We all know this, right?
It’s a simple concept. It sounds correct. And we’ve all heard it, right?
We have.
But it’s also wrong.
This is one of those zombie ideas: easily repeated and rarely examined.
And as such almost impossible to kill.
This misinformation shuffles around endlessly in beginner blogs. People mindlessly repeat it in post after post on StackOverflow and Reddit. And it’s constantly regurgitated and respawned in article after AI-generated “article”.
It feels like common knowledge.
But it completely and totally misrepresents what’s actually going on under the hood.
Let’s fix that.
Value Semantics Are Not COW
Start with the basics: Swift makes a clear distinction between value semanticsand reference semantics.
When we say a type has value semantics, we mean that when we assign it to another variable or pass it along to a function, that new value behaves like it was a copy. Changing it has no affect on the original.
That’s the behavior developers see and rely on. And Swift enforces it.
But the implementation? How it works? That’s another matter.
Most trivial Swift types like Int
, Double
are value types that are copied outright. And that’s also true of your average garden-variety struct, something like the following Point
.
struct Point {
var x: Int
var y: Int
}
var a = Point(x: 1, y: 2)
var b = a
b.x = 10
print(a.x) // 1 — a is unaffected
Assigning a
to b
means copying the value, byte-by-byte*. Pass one as a parameter to a function? Also copied.
No zombie COWs. No clever tricks. Just copies.
You change one instance and the other one doesn’t change, because it’s literally a different object in memory.
But What About Strings and Arrays?
Ah yes, here’s where the myth gets a foothold in reality.
You see, some types in the Swift Standard Library, entities like String
, Array
, Dictionary
, Set
, and Data
, do implement copy-on-write.
They have value semantics and they behave like value types, but behind the scenes they’re structs that use reference-counted heap storage for their data buffers.
Don’t believe me? Here’s the root definition of an Array from the Swift Standard Library.
public struct Array<Element>: _DestructorSafeContainer {
#if _runtime(_ObjC)
@usableFromInline
internal typealias _Buffer = _ArrayBuffer<Element>
#else
@usableFromInline
internal typealias _Buffer = _ContiguousArrayBuffer<Element>
#endif
@usableFromInline
internal var _buffer: _Buffer // INTERNAL BUFFER
/// Initialization from an existing buffer does not have "array.init"
/// semantics because the caller may retain an alias to buffer.
@inlinable
internal init(_buffer: _Buffer) {
self._buffer = _buffer
}
}
Every value appended to an array is appended to that internally managed buffer.
So where does COW come into play? Well, the Swift team hand-rolled copy-on-write logic into these types so you get the performance of shared storage and the safety of value semantics.
public mutating func append(_ newElement: __owned Element) {
// Separating uniqueness check and capacity check allows hoisting the
// uniqueness check out of a loop.
_makeUniqueAndReserveCapacityIfNotUnique() // COW!!!
let oldCount = _buffer.mutableCount
_reserveCapacityAssumingUniqueBuffer(oldCount: oldCount)
_appendElementAssumeUniqueAndCapacity(oldCount, newElement: newElement)
_endMutation()
}
Which leads us to the truth. COW isn’t a general rule or behavior built into allstructs. It’s a deliberate engineering decision, specific to certain types, done solely make them efficient without violating value behavior.
The copy-on-write mechanism isn’t “built into the Swift compiler” in some abstract way. It’s custom code. Those buffers are managed manually. Checks are added to detect when the reference count becomes greater than one, and that’s when a real copy is made.
It’s an optimization. Nothing more. Nothing less.
And one that’s clever, important, and easy to misunderstand.
Most Structs Don’t Do This
As mentioned, the vast majority of our own structs don’t have shared backing buffers. They don’t do their own reference counting.
And they don’t magically avoid copies unless you add that behavior yourself.
Assign a Point
? Swift copies it.
Pass it into a function? Swift copies it.
Mutate it after a copy? You’re changing a different memory location, because the copy already happened.
Unless you start using inout
references, unsafe pointers, reference wrappers, or your own COW buffer types, there is no hidden mechanism waiting to save you from full value copying.
What If My Struct Contains a Reference Type?
Now we get into subtler territory.
Let’s say you embed a class in your struct:
class Counter {
var count = 0
}
struct Wrapper {
var counter = Counter()
}
Now try this:
var a = Wrapper()
var b = a
b.counter.count += 1
print(a.counter.count) // 1
Value semantics? Nope. Even though Wrapper
is a struct, it contains a reference. The reference is copied, but both a
and b
still point to the same Counter
instance. This is a shallow copy.
The pointer is copied and ARC increases the reference count, but the memory itself is shared.
This is not COW. There’s no mutation barrier. You mutate count
onb
, and a
sees it, but isn’t notified of the change. That’s what happens when you embed reference types inside of value types: your value semantics are only as deep as those of your members.
Performance
There are performance aspects to this as well. What happens when a struct, for example, contains dozens upon dozens of Strings? Names, addresses, phone numbers, email addresses, and the like?
Well, when it’s copied, all of those string pointers are copied… and then each and every string buffer needs to have its reference count increased.
Throw that copy away or let it go out of scope? Decrement all of those reference counts.
If you want true isolation, you have to implement your own buffers and write your COW logic yourself. That means tracking reference counts, checking for uniqueness (see _isUniquelyReferenced
), and copying your properties manually before mutation.
*Byte-By-Byte
I insinuated above that struct values are copied byte-by-byte, and that’s true, usually through a library function like memcpy
.
But… there’s an exception to that rule as well.
Simple structs that contain trivial, Plaid-Old-Data (POD) types like Int
, Double
, Bool
, and so on can be copied using that mechanism, usually referred to in compiler-speak as a bitwise copy.
But if a struct contains non-trivial fields, then the compiler can no longer assume it’s POD-safe. Look at PodStruct
below.
struct PodStruct {
var x: Int
var str: String
}
That struct is not POD and that’s because String
is not trivial. It has reference-counted storage under the hood, and the compiler must respect ARC semantics, including potentially incrementing the reference count during a copy.
So if all of the stored properties of a struct are trivial (POD), then the struct can be copied via memcpy
.
But if any property is non-trivial (ARC-managed reference, COW wrapper, has willSet/didSet components, or basically anything requiring custom copy logic), then Swift needs to generate a set of member-wide assignment operations.
In fact, Swift explicitly defines “trivial” vs “non-trivial” types in its ABI. You can only get bitwise copying behavior for trivial structs.
The Compiler May Optimize Copies, But That’s Also Not COW
To make things even trickier, the Swift compiler can eliminate redundant copies through optimization passes.
For example, if you pass a struct into a function but only read a single field, the compiler might decide to optimize away the need to copy the entire struct. It might just pass that one value.
Or, in the case of our basic two-valuePoint
struct above, it may simply shove x
and y
into their own registers and pass it them along that way.
But keep in mind that these behaviors are optimizations, not language rules. They’re not guaranteed and they don’t rely on or resemble copy-on-write.
It’s just smart code generation.
Borrow Semantics
Just for completeness, Swift’s new borrow semantics (~copyable) allow the compiler to pass struct references without copying if it’s declared as borrowing
That improves performance by not copying and by avoiding any ARC reference count increments/decrements that might have been required.
But again, this is yet another compile-time optimization and it’s different from COW or other explicit copying mechanisms.
So Why Do People Keep Saying Structs Use COW?
Because it’s easy. It’s one of those tidy little mental models that works well enough in casual conversation, spreads like wildfire through Stack Overflow and bootcamp slides, and then that garbage gets fed to AI systems ravenous for more brains. Err.. training data.
But that’s the problem: beginners hear it and assume that all structs behave like Array
. Others start believing that Swift structs are secretly really reference types with some copy-on-write compiler magic baked in.
That’s false, and dangerously misleading, especially when you start writing performance-sensitive code.
LLMs
Worse, and as mentioned, LLMs like ChatGPT have learned this misinformation verbatim.
Ask one what happens when you assign or pass a struct in Swift, and you’ll likely get a supremely confident, albeit wrong answer: “Structs in Swift use copy-on-write to avoid unnecessary duplication.”
Rinse and repeat.
It’s not malicious. It’s just bad training data.
It’s just statistically repeating what it’s been told. Why? Because it’s been trained on those very same Medium articles and StackOverflow posts.
And it assumes those answers are correct.
The Actual Rule
Swift structs have value semantics. That’s the guarantee.
Copy-on-write is an implementation detail for a few specific types in the standard library that are engineered to provide it.
Unless you go out of your way to implement copy-on-write behavior yourself, your structs will be copied. Every time you assign them. Every time you pass them into a function.
That’s not a problem. It’s just how value types work.
If you’re repeating the myth, stop. If you see someone else repeating it, correct them. If you’re asking ChatGPT about it… you probably deserve the answer you’re going to get.
It’s time to kill the zombie cows.
Completion Block
That’s it for this one.
Like many recently, it was triggered by another outbreak, an AI-generated article that contained the zombie COW meme.
We really need to do something about this.
So what do you think? Let me know in the comments below.