How do generics work under the hood in Swift?
The core idea: generics are compiled once, dispatched via witness tables
Unlike C++ templates (monomorphization), Swift generics are, by default, compiled to a single, shared implementation that works for any conforming type at runtime, via witness tables plus type metadata.
1. Protocol witness tables (PWTs)
When a type conforms to a protocol, the compiler generates a witness table for that conformance — a struct of function pointers, one per protocol requirement.
protocol Drawable { func draw() }
struct Circle: Drawable { func draw() { /* ... */ } }
func render<T: Drawable>(_ shape: T) {
shape.draw()
}
The compiler compiles one version of render that takes an extra hidden parameter: a pointer to the witness table for whatever T turns out to be.
2. Type metadata — how the function knows the size/layout of T
The function also needs a type metadata pointer — size, alignment, and value-witness functions (copy, destroy) for the concrete type. Effectively:
render(shape: OpaquePointer, T_metadata: TypeMetadata*, T_Drawable_witness: WitnessTable*)
3. Specialization — when Swift does generate type-specific code
If the compiler can see the concrete type at the call site (same module, or @inlinable/whole-module optimization), it can generate a fully specialized version — direct static calls, no witness table indirection, inlinable.
4. This differs sharply from existentials (any Protocol)
- Generics: concrete type fixed for a given call, resolved via witness tables passed as hidden parameters.
- Existentials: concrete type can vary at runtime within the same variable, handled via an existential container (3 words inline, heap-boxed if larger) plus pointers to type metadata and witness table.
5. Associated types and some (opaque types)
Protocols with associated types can’t be used as existentials directly in most contexts, because the existential container has no fixed way to represent an unknown associated type’s metadata. some Protocol solves a related problem — the caller doesn’t know the concrete type, but the compiler does, so it can still use the fast generics-style witness table path.
6. Generic specialization and code size tradeoffs
Aggressive specialization is a code-size vs. speed tradeoff — whole-module optimization can specialize generics extensively, speeding execution but increasing binary size.
Interview-ready summary
| Mechanism | How type info is passed | Dispatch cost | Flexibility |
|---|---|---|---|
| Generics (unspecialized) | Witness table + metadata passed as hidden params | Indirect call through table | Type fixed per call |
| Generics (specialized) | Resolved at compile time | Direct call, inlinable | Same as hand-written code |
Existentials (any P) | Stored with the value in a container (inline or boxed) | Indirect call, possible heap alloc | Concrete type can vary at runtime |
some P (opaque) | Fixed at definition site, hidden from caller | Same as specialized generics | Type fixed but caller doesn’t know which |
One-liner if pressed: “Swift generics use a witness-table-and-metadata-passing model — the compiler emits one implementation per generic function, and calls into the right methods indirectly via a witness table the caller supplies, unless the compiler can see the concrete type at the call site and specializes it into direct, inlinable code.”