The problem: one name, one type
Guide 1 walked through what C11, C17, and C23 added; this guide zooms all the way into one of those additions and takes it apart. Start with the itch it scratches. In C, a function name means exactly one function with one fixed signature. If you write `int max_i(int a, int b)` and later need the same idea for `double`, you must write a whole second function — `double max_d(double a, double b)` — and remember to call the right one by name. There is no overloading: the name `max` cannot quietly mean two functions at once the way it can in C++ or Python. From C89 onward this was simply a fact of life, and the standard library shows the scars — `abs()` for `int`, `labs()` for `long`, `fabs()` for `double`, three names for one idea.
The older escape hatch was a function-like macro: `#define MAX(a,b) ((a)>(b)?(a):(b))`. That does work for any type, because — as you learned in the toolchain rung — a macro is blind text substitution and never looks at types at all. But blindness cuts both ways. The macro will just as cheerfully expand `MAX(p, "hello")` into nonsense, gives you no real type checking, and double-evaluates its arguments so `MAX(i++, j)` bites. What we actually want is a single name that inspects the type of its argument and picks the right code — and to do that decision at compile time, not at runtime. That is precisely the gap C11 filled.
What _Generic actually is
C11 added one keyword for this, spelled `_Generic`, and the feature is called a generic selection. Picture it as a `switch` that branches on a type instead of a value. You hand it a controlling expression, then a list of `type : result` pairs. The compiler looks at the type of that expression, finds the matching pair, and the whole `_Generic(...)` is replaced — right there during compilation — by just that one chosen result. The other branches are not merely skipped at runtime; they are thrown away entirely and never compiled into your program. It is a compile-time chooser, and it leaves no trace at runtime: zero branches, zero cost.
_Generic( EXPR ,
int : "it is an int",
double : "it is a double",
char * : "it is a char pointer",
default: "something else"
)
/* the compiler examines the TYPE of EXPR,
then replaces the whole thing with exactly
ONE of the four string results -- at compile time. */Three details matter and are easy to miss. First, the controlling expression is examined for its type only — it is never evaluated, exactly like the operand of sizeof. So `_Generic(i++, ...)` does not increment `i`; it just asks "what type is `i`?" That single fact already fixes the double-evaluation curse of the macro `MAX`. Second, each `result` can be any expression — a string, a number, or, as we'll see, a function name. Third, the type labels match the type after the usual conversions C applies to a value expression, a subtlety we return to in a moment. Without that, half your selections would land on the wrong branch.
Wrapping it in a macro: the real pattern
On its own `_Generic` is awkward — you would have to write the controlling expression twice, once to select on and once to actually pass. So in practice it almost always lives inside a function-like macro, and the two features finally play to each other's strengths. The macro captures the argument once as a parameter; the `_Generic` inspects that parameter's type and selects which real function to call. Here is the classic `print` that dispatches by type:
void print_int(int v);
void print_dbl(double v);
void print_str(char *v);
#define print(x) _Generic((x), \
int : print_int, \
double : print_dbl, \
char * : print_str \
)(x)
/* print(42) becomes print_int(42)
print(3.14) becomes print_dbl(3.14)
print("hello") becomes print_str("hello") */Read that macro slowly, because the trick is in the layout. The `_Generic(...)` itself evaluates to one of the three function names — that is the result of each branch. Then the `(x)` written after the closing parenthesis is an ordinary function call applied to whatever name got chosen. So `print(3.14)` expands first to `_Generic((3.14), ...)(3.14)`, the selection collapses to `print_dbl`, and you are left with `print_dbl(3.14)`. Each branch's result is a function name precisely so this final call works; the design leans on the fact that a function's name decays to a pointer you can immediately call. Note that the macro still evaluates `x` twice — once for selection (harmless, never evaluated) and once in the call (evaluated normally) — so the no-side-effects discipline from the toolchain rung still applies to the argument you pass.
The traps: conversions, pointers, and missing matches
Now the honest part, because `_Generic` has sharp edges that surprise everyone the first time. The most important is that the controlling expression's type is taken after the usual conversions a value undergoes — the same integer promotions and conversions you met in the data rung, plus array-to-pointer and function-to-pointer decay. Two consequences bite often. A `char` or `short` argument is promoted to `int` before matching, so a `char : ...` branch may simply never be reached. And a string literal or an array argument decays, so `"hello"` matches `char *`, not `char[6]`. If your labels don't account for this, your value lands on `default` or fails to compile, and you stare at it baffled.
The pointer rules are stricter than C++ programmers expect. The match is by exact type, with no implicit qualifier dance: `char *` and `const char *` are two different labels, and an argument of one will not match the other. Likewise `int *` and `void *` are distinct. This precision is a feature — it is how `_Generic` gives you the type checking the old `MAX` macro never could — but it means you sometimes have to spell out more branches than you'd guess, including separate `const`-qualified ones if your code mixes them.
Finally, the missing-match rule. If the controlling expression's type matches none of your labels and you wrote no `default`, that is a compile-time error, not a silent fallthrough — which, honestly, is exactly what you want. It means a type you forgot to handle gets caught by the compiler instead of misbehaving at runtime. Add a `default :` branch only when you genuinely have a sensible catch-all; otherwise let the hard error protect you. This makes `_Generic` far safer than the type-blind macro it replaces: wrong types are rejected at build time, in the spirit of the fail-fast checking the error-handling rung argued for.
C23 polish, and how to think about it
C23 sands down two rough edges. The first is the spelling. As guide 1 noted, C23 lets you write the friendlier keyword `generic` (and the lowercase forms of the other underscore-capital keywords) instead of the shouty `_Generic`, by default — the ugly underscore name was only ever there to avoid breaking old code that might have used `generic` as an identifier. The second is a genuinely useful new companion: typeof, standardized in C23, which gives you the type of an expression for use in declarations. Pairing `typeof` with `_Generic` lets you both select on a type and name that same type, for instance to declare a temporary with exactly the argument's type inside a generic macro.
It helps to place `_Generic` honestly among its relatives. It is not C++ templates: a template generates whole new code per type and can deduce types deeply, while `_Generic` is a flat, finite, hand-written menu of types you must list out yourself. It is not overloading either; it is one expression chosen at compile time. Think of it as a precise, low-level switch on type — closer to a clever macro that finally respects types than to anything from a language with real generics. That modest scope is exactly why it costs nothing at runtime and why you can read, in one screen, every type it will ever handle.