What does the compiler turn a property wrapper into: what storage exists, what are $name and _name, and what does it cost?

5.10.1 · L5 · Learning

Predict first

What does this print?

@propertyWrapper struct Clamped {
    private var v: Int
    var wrappedValue: Int { get { v } set { v = min(newValue, 10) } }
    var projectedValue: Bool { v == 10 }
    init(wrappedValue: Int) { v = min(wrappedValue, 10) }
}
struct S { @Clamped var n = 5 }
var s = S()
s.n = 99
print(s.n, s.$n, MemoryLayout<S>.size, Mirror(reflecting: s).children.map { $0.label! })
  • 10 true 8 ["_n"]
  • 99 false 8 ["n"]
  • 10 true 16 ["n", "$n"]
  • 10 true 1 ["_n"]

Predict first

What synthesised declarations does the AST contain?

import Swift
@propertyWrapper struct Clamped {
    private var v: Int
    var wrappedValue: Int { get { v } set { v = min(newValue, 10) } }
    var projectedValue: Bool { v == 10 }
    init(wrappedValue: Int) { v = min(wrappedValue, 10) }
}
struct S { @Clamped var n = 5 }
  • an implicit _n of type Clamped (private) and $n of type Bool
  • only n
  • a class named S_n
  • nothing extra

Mental model. A property wrapper is a mailroom clerk standing in front of your mailbox: you say “put this in n” and the clerk (the wrapper) applies the rules and files it in a box named _n that you never see.

A property wrapper is a rewrite. For @Clamped var n = 5 the compiler synthesises a private stored property _n of the wrapper type, and turns n into a computed property whose getter and setter call _n.wrappedValue. If the wrapper has a projectedValue, it also adds $n, which returns it. The initial value 5 is passed to init(wrappedValue:) when the instance is created. The AST dump shows exactly this: an implicit _n of type Clamped and an implicit $n of type Bool. The run confirms the consequences: the struct’s size is that of the wrapper’s storage (8 bytes), and Mirror reports the child as _n, not n.

Costs and traps follow. A wrapper with simple stored state is optimised away in -O like hand-written accessors, but one that holds a class, a lock or an allocation pays that on every instance. Because the real storage is _n, reflection, synthesised Codable (which uses the wrapper’s own conformance), memberwise initialisers (which take the wrapped type unless you opt in) and KVC-style tools see the wrapper, not the name you wrote. Wrappers cannot be applied to a computed property or used where the type has to be inferred from several declarations, and in Swift 6 the wrapper’s isolation and Sendable-ness is checked on the synthesised storage. @State, @Published and @AppStorage are ordinary wrappers with extra runtime hooks.

n clamps to 10, $n is the projected value, storage is one Int named _n.

The compiler adds backing storage and the projection.

Common traps

  • Expecting Mirror or logging to show the property name. You see _n.
  • Putting a heavy reference type in a wrapper used by many value types. Each instance pays for it.

They might follow up with

  • How does SwiftUI know when @State changes? The wrapper’s storage lives outside the view value and signals the update; the wrapper pattern is only the entry point.
  • Can wrappers compose? Yes, each layer wraps the next; order matters.

Related questions

  • 1.6.3 What is a property wrapper? Can you write a custom one?
  • 1.6.5 Are property wrappers thread-safe? Is lazy?
  • 1.10.3 Macros vs property wrappers vs result builders — one sentence each.
  • 5.8.4 What reflection metadata does the compiler emit for every type, what depends on it, and what does…
  • 3.4.2 What do MemoryLayout’s size, stride and alignment mean, and why does field order matter?

What does the compiler do with the statements inside a result builder… →