JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Type-Generic Code with _Generic

C has always been stubbornly monomorphic: one name, one type. C11 quietly handed it a single new tool — _Generic — that lets one call pick different code based on the type of its argument, all decided at compile time with zero runtime cost. This guide takes that tool apart and shows exactly how it works.

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. */
The anatomy of a generic selection: a controlling expression, then type:result pairs and an optional default.

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")   */
_Generic selects a function NAME; the trailing (x) then calls whichever was chosen.

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.