SwiftQA Random

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

MechanismHow type info is passedDispatch costFlexibility
Generics (unspecialized)Witness table + metadata passed as hidden paramsIndirect call through tableType fixed per call
Generics (specialized)Resolved at compile timeDirect call, inlinableSame as hand-written code
Existentials (any P)Stored with the value in a container (inline or boxed)Indirect call, possible heap allocConcrete type can vary at runtime
some P (opaque)Fixed at definition site, hidden from callerSame as specialized genericsType 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.”