Type-Generic Programming

This section documents C23 (ISO/IEC 9899:2024), per ISO/IEC JTC1/SC22/WG14’s freely available working draft N3220, which WG14 documents as differing from the published standard only editorially — the reference these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against the WG14 draft and cppreference.com’s C reference before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

C has no templates and no overloading, yet the standard library manages sqrt for every floating type and qsort for every array. It does that with four mechanisms: implicit conversions, void , function pointers, and — since C11 — *generic selection with _Generic. C23 adds typeof and auto, which together make type-generic macros far more pleasant to write.

What C Is Already Generic About

Before reaching for _Generic, note how much is generic by construction:

  • Operators work on every arithmetic type, with the usual arithmetic conversions picking the common type — see Basic Types and Values.

  • void * holds a pointer to any object, so an algorithm can move bytes without knowing their type. That is how memcpy, qsort and bsearch work.

  • Function pointers let the caller supply the type-specific part — a comparator, a hash, a destructor.

  • sizeof makes the element size a run-time value, which is what lets one function walk any array.

A generic container in plain C is exactly those three ideas combined:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// A type-agnostic dynamic array: element size and a copy of the bytes.
struct AnyVec {
    unsigned char *data;
    size_t element_size;
    size_t count;
    size_t capacity;
};

static bool anyvec_init(struct AnyVec *v, size_t element_size)
{
    if (element_size == 0) {
        return false;
    }
    v->data = nullptr;
    v->element_size = element_size;
    v->count = 0;
    v->capacity = 0;
    return true;
}

static bool anyvec_push(struct AnyVec *v, const void *element)
{
    if (v->count == v->capacity) {
        size_t new_capacity = v->capacity ? v->capacity * 2 : 4;
        unsigned char *grown = realloc(v->data, new_capacity * v->element_size);
        if (grown == nullptr) {
            return false;
        }
        v->data = grown;
        v->capacity = new_capacity;
    }

    memcpy(v->data + v->count * v->element_size, element, v->element_size);
    ++v->count;
    return true;
}

static void *anyvec_at(struct AnyVec *v, size_t index)
{
    return v->data + index * v->element_size;
}

int main(void)
{
    struct AnyVec v;
    if (!anyvec_init(&v, sizeof(double))) {
        return EXIT_FAILURE;
    }

    for (int i = 0; i < 3; ++i) {
        double value = i * 1.5;
        if (!anyvec_push(&v, &value)) {
            free(v.data);
            return EXIT_FAILURE;
        }
    }

    printf("%zu elements, [2] = %g\n", v.count, *(double *)anyvec_at(&v, 2));
    free(v.data);
    return 0;
}

The cost is real: no type checking on anyvec_push, an indirection per access, and a cast at every read. That is the trade C makes — and why type-specific code generated by a macro is the other common approach.

_Generic — Generic Selection

_Generic is a compile-time selection on the type of a controlling expression. It is an expression, not a statement, and only the selected branch is part of the program:

#include <stdio.h>

// Map a type to a printf conversion specifier.
#define FORMAT_OF(x) _Generic((x),          \
    char:               "%c",               \
    signed char:        "%hhd",             \
    unsigned char:      "%hhu",             \
    short:              "%hd",              \
    int:                "%d",               \
    unsigned int:       "%u",               \
    long:               "%ld",              \
    unsigned long:      "%lu",              \
    long long:          "%lld",             \
    unsigned long long: "%llu",             \
    float:              "%g",               \
    double:             "%g",               \
    long double:        "%Lg",              \
    char *:             "%s",               \
    const char *:       "%s",               \
    void *:             "%p",               \
    default:            "%p")

#define PRINT(x) printf(FORMAT_OF(x), (x)), putchar('\n')

// A type name, for diagnostics.
#define TYPE_NAME(x) _Generic((x),  \
    int:      "int",                \
    double:   "double",             \
    char *:   "char *",             \
    default:  "something else")

int main(void)
{
    int i = 42;
    double d = 3.5;
    const char *s = "text";

    PRINT(i);
    PRINT(d);
    PRINT(s);

    printf("%s %s %s\n", TYPE_NAME(i), TYPE_NAME(d), TYPE_NAME(1.0f));
    return 0;
}

Dispatching to functions is the more common use — this is how you give one name to several implementations:

#include <math.h>
#include <stdio.h>

static int abs_int(int v)          { return v < 0 ? -v : v; }
static long abs_long(long v)       { return v < 0 ? -v : v; }
static double abs_double(double v) { return fabs(v); }

// One name, three implementations, resolved at compile time with no run-time cost.
#define ABS(x) _Generic((x),    \
    int:    abs_int,            \
    long:   abs_long,           \
    float:  abs_double,         \
    double: abs_double)(x)

int main(void)
{
    printf("%d %ld %g %g\n", ABS(-5), ABS(-5L), ABS(-5.5), ABS(-5.5f));
    return 0;
}

The rules that trip people up:

  • The controlling expression is not evaluated — only its type is used. It still undergoes lvalue conversion, so an array argument matches a pointer type and a const int variable matches int.

  • Every association must be a distinct, complete type; you cannot associate a VLA type, and default is optional but recommended.

  • All branches must parse, even the unselected ones — so a branch that would be invalid for the other types must be hidden behind a function call, not written inline.

  • String literals have type char[N], which decays to char *, not const char *.

<tgmath.h>

The standard library’s own use of generic selection: one name per maths function, dispatching over float, double, long double and the complex types.

#include <stdio.h>
#include <tgmath.h>

int main(void)
{
    float f = 2.0f;
    double d = 2.0;
    long double ld = 2.0L;

    // One name; sqrtf, sqrt or sqrtl is selected by the argument's type.
    printf("%g %g %Lg\n", (double)sqrt(f), sqrt(d), sqrt(ld));
    printf("%g %g\n", (double)pow(f, 2.0f), fabs(-d));
    return 0;
}

Include <tgmath.h> instead of <math.h> when you want that — it includes <math.h> and <complex.h> and then defines the type-generic macros over them. In new code many people prefer the explicit sqrtf/sqrtl spellings, because the macro hides which function actually runs.

typeof and typeof_unqual (C23)

typeof(expr) is the type of an expression — a GCC extension since forever, standardized in C23. It makes a macro able to declare a temporary of the right type:

#include <stdio.h>

// A safe MAX: each argument is evaluated exactly once, because each gets its own
// temporary of the correct type. This is what typeof buys you.
#define MAX(a, b)                       \
    ({                                  \
        typeof(a) max_a_ = (a);         \
        typeof(b) max_b_ = (b);         \
        max_a_ > max_b_ ? max_a_ : max_b_; \
    })

// typeof_unqual strips const/volatile/_Atomic -- needed to declare a WRITABLE copy.
#define COPY_OF(x) ((typeof_unqual(x))(x))

int main(void)
{
    int i = 3;
    int m = MAX(i++, 2);                // i is incremented once
    printf("%d, i = %d\n", m, i);

    const int ci = 7;
    typeof_unqual(ci) writable = ci;    // int, not const int
    writable = 8;

    typeof(&i) pointer_to_int = &i;     // int *
    typeof(main) *fn = main;            // pointer to this function's type

    printf("%d %d %p\n", writable, *pointer_to_int, (void *)fn);
    printf("%d\n", COPY_OF(ci));
    return 0;
}

The MAX macro above uses a statement expression (({ … })), which is a GCC/Clang extension rather than standard C — it is the only way to declare a temporary inside an expression today. In strictly portable code, use a static inline function per type plus a _Generic dispatch instead. typeof itself is standard C23.

auto Type Inference (C23)

In C23 auto — previously a no-op storage-class specifier nobody wrote — means "infer the type from the initializer":

#include <stdio.h>

int main(void)
{
    auto count = 42;                    // int
    auto ratio = 1.5;                   // double
    auto text = "hello";                // char *
    auto sum = count * ratio;           // double

    // The real use: naming an unwieldy type without repeating it.
    int values[3] = { 1, 2, 3 };
    auto p = &values[0];                // int *

    for (auto i = 0u; i < 3u; ++i) {    // unsigned int
        printf("%d ", p[i]);
    }
    putchar('\n');

    printf("%d %g %s %g\n", count, ratio, text, sum);
    return 0;
}

C’s auto is deliberately narrower than C++'s: it needs an initializer, cannot be used for parameters or return types, and cannot be combined with other type specifiers. Use it where the type is obvious and long, not to hide what a variable is.

Where C Is Heading

Two further directions worth knowing about, neither in C23:

  • Statement expressions (({ … })) and nested functions are GCC/Clang extensions that make macros behave more like functions. Widely used (the Linux kernel’s min/max rely on them) but non-standard.

  • Lambdas / compound literals for functions have been proposed for C2y (WG14 N3199 and related papers), which would give callbacks a way to capture context without a void *user_data parameter.

Until then, the practical toolkit is: static inline functions for the type-specific work, _Generic to give them one name, typeof to write hygienic macros, and void * plus a function pointer when the type genuinely must be decided at run time.

See Also