Procedural Programming with C · Advanced · Week 30

Function Pointers, Callbacks, and Generic Programming

C has no templates, no interfaces, and no overloading. What it has are function pointers and void * — and between them they are enough to build everything those features provide, with the mechanism left visible.

By the end of this week you can
  • Declare, assign, and call a function pointer, and read its declaration.
  • Use qsort and bsearch with comparators you write.
  • Build a dispatch table and simulate polymorphic behavior.
  • Write a container that works over any element type through void *.
  • Write a variadic function, and explain why it cannot check its arguments.

1Function pointers

A function has an address, so a pointer can hold it:

int add(int a, int b) { return a + b; }

int (*operation)(int, int) = add;      /* declare and assign */
int result = operation(3, 4);          /* call through it    */

Apply week 16's right-left rule to int (*operation)(int, int): start at operation, the parentheses force left to * — a pointer to — then right to (int, int) — a function taking two ints — then left to int — returning int. The parentheses are essential: without them, int *operation(int, int) declares a function returning int *.

The & and * are optional in both directions. operation = &add and (*operation)(3, 4) are legal and mean the same as the shorter forms. Most code omits them.

A typedef makes the type usable:

typedef int (*BinaryOp)(int, int);

BinaryOp operation = add;
void apply_all(const int *v, size_t n, BinaryOp op);

This is the one case where hiding a pointer behind a typedef is standard practice, because the raw declaration is genuinely hard to read.

2Callbacks and qsort

A callback is a function you pass to another function so it can call you back. The standard library's sort is the canonical example:

void qsort(void *base, size_t count, size_t size,
           int (*compare)(const void *, const void *));

qsort knows nothing about your element type. It receives the base address, the number of elements, the size of one, and a function that can order two of them. Everything type-specific lives in the comparator.

static int compare_int(const void *a, const void *b)
{
    int x = *(const int *)a;
    int y = *(const int *)b;
    return (x > y) - (x < y);      /* -1, 0, or 1 without overflow */
}

qsort(values, n, sizeof values[0], compare_int);

Do not write return x - y;. It is the textbook comparator and it overflows: with x = INT_MAX and y = -1 the subtraction is undefined behavior and the sign can come out wrong, so the sort silently produces a wrong order. (x > y) - (x < y) is branch-free, cannot overflow, and yields exactly −1, 0, or 1.

bsearch has the same shape and searches a sorted array with the same comparator:

int key = 42;
int *found = bsearch(&key, values, n, sizeof values[0], compare_int);

Two requirements people forget: the array must already be sorted by the same comparator, and the key is passed by address, because the comparator takes const void *.

3Dispatch tables

An array of function pointers replaces a long switch with a lookup:

typedef struct {
    const char *name;
    int (*handler)(int, int);
} Command;

static const Command commands[] = {
    { "add", add }, { "sub", sub }, { "mul", mul }
};

for (size_t i = 0; i < COUNT; i++) {
    if (strcmp(input, commands[i].name) == 0) {
        return commands[i].handler(a, b);
    }
}

The advantage is not speed — a switch over small integers compiles to a jump table anyway. It is that the table is data: adding a command is one line in one place, the set can be iterated to produce a help listing, and a plugin can extend it at run time.

Polymorphism

Put function pointers inside a structure and you have virtual methods:

typedef struct Shape {
    const char *name;
    double (*area)(const struct Shape *);
    double (*perimeter)(const struct Shape *);
} Shape;

typedef struct {
    Shape base;            /* must be first: layout matters */
    double radius;
} Circle;

static double circle_area(const Shape *s)
{
    const Circle *c = (const Circle *)s;      /* safe: base is first */
    return 3.14159265358979 * c->radius * c->radius;
}

Because the base structure is the first member, a Circle * and a Shape * point at the same address, so the cast is well defined. This is precisely how C++ implements single inheritance, and how the Linux kernel's device model works — the mechanism is the same, only written out.

4Generic containers with void *

A container that stores elements of unknown type must know three things: where an element is, how large it is, and how to operate on it.

typedef struct {
    void   *data;          /* raw bytes             */
    size_t  count;
    size_t  capacity;
    size_t  element_size;  /* supplied at creation  */
} Vector;

static void *element_at(const Vector *v, size_t i)
{
    return (char *)v->data + i * v->element_size;   /* char *: byte arithmetic */
}

The cast to char * is essential. Pointer arithmetic on void * is not standard C — GCC allows it as an extension, treating it as byte arithmetic, but portable code converts to char *, whose element size is 1 by definition.

GainCost
One implementation for every typeNo type checking at all
Works with any element sizeEvery access needs a cast
Standard, portablememcpy per element instead of assignment

The alternative is a macro that generates a typed container per element type — type-safe and fast, at the cost of unreadable macros and duplicated code. Both approaches are used in real projects; void * is the simpler one to get right.

5Variadic functions

#include <stdarg.h>

int sum_all(int count, ...)
{
    va_list args;
    va_start(args, count);          /* last NAMED parameter */

    int total = 0;
    for (int i = 0; i < count; i++) {
        total += va_arg(args, int); /* you state the type */
    }

    va_end(args);
    return total;
}

Three rules. There must be at least one named parameter, and va_start names it. You must state each argument's type in va_arg, because nothing at run time records it. And va_end must be called before returning.

The function has no way to know how many arguments it received, so the count must arrive some other way — an explicit count, a sentinel value such as NULL, or a format string. That last option is what printf does, and it explains week 8's warning: a wrong specifier makes printf read the wrong number of bytes as the wrong type, and nothing can detect it.

Arguments also undergo default promotions: float becomes double, and types narrower than int become int. So va_arg(args, float) is always wrong — use double. Likewise va_arg(args, char); use int.

To wrap a printf-like function, use the v variants that take a va_list:

void log_message(const char *fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    vfprintf(stderr, fmt, args);    /* vfprintf, not fprintf */
    va_end(args);
}

6_Generic

C11's _Generic selects an expression based on the type of a controlling expression, at compile time:

#define type_name(x) _Generic((x),        \
    int:         "int",                   \
    double:      "double",                \
    char *:      "char *",                \
    const char *: "const char *",         \
    default:     "something else")

#define absolute(x) _Generic((x), \
    int:    abs,                  \
    long:   labs,                 \
    double: fabs)(x)

This gives type-based dispatch with no runtime cost — the compiler picks one branch and discards the rest. It is how <tgmath.h> provides type-generic mathematics, and it is the closest C comes to overloading.

The limits are real: it selects on type only, the branches must all be valid expressions, and the syntax becomes unreadable past a handful of cases. Use it for thin wrappers, not as a general dispatch mechanism.

7Worked example: the week 19 array, made generic

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <stdbool.h>
#include <math.h>

/* ================= generic vector ================= */

typedef struct {
    void   *data;
    size_t  count;
    size_t  capacity;
    size_t  element_size;
} Vector;

static bool vector_init(Vector *v, size_t element_size, size_t capacity)
{
    if (capacity == 0) capacity = 4;
    v->data = malloc(capacity * element_size);
    if (v->data == NULL) return false;
    v->count = 0;
    v->capacity = capacity;
    v->element_size = element_size;
    return true;
}

static void vector_destroy(Vector *v)
{
    free(v->data);
    v->data = NULL;
    v->count = v->capacity = 0;
}

static void *vector_at(const Vector *v, size_t i)
{
    return (char *)v->data + i * v->element_size;   /* byte arithmetic */
}

static bool vector_push(Vector *v, const void *element)
{
    if (v->count == v->capacity) {
        size_t bigger = v->capacity * 2;
        void *moved = realloc(v->data, bigger * v->element_size);
        if (moved == NULL) return false;
        v->data = moved;
        v->capacity = bigger;
    }
    memcpy(vector_at(v, v->count), element, v->element_size);
    v->count++;
    return true;
}

/* A callback applied to every element: the visitor pattern in C. */
static void vector_each(const Vector *v, void (*visit)(const void *, void *),
                        void *context)
{
    for (size_t i = 0; i < v->count; i++) {
        visit(vector_at(v, i), context);
    }
}

static void vector_sort(Vector *v, int (*cmp)(const void *, const void *))
{
    qsort(v->data, v->count, v->element_size, cmp);
}

/* ================= comparators ================= */

typedef struct { char name[16]; int score; } Student;

/* Never x - y: it overflows. */
static int cmp_int(const void *a, const void *b)
{
    int x = *(const int *)a, y = *(const int *)b;
    return (x > y) - (x < y);
}

static int cmp_double(const void *a, const void *b)
{
    double x = *(const double *)a, y = *(const double *)b;
    return (x > y) - (x < y);
}

static int cmp_student_score(const void *a, const void *b)
{
    const Student *x = a, *y = b;
    return (y->score > x->score) - (y->score < x->score);   /* descending */
}

static int cmp_student_name(const void *a, const void *b)
{
    return strcmp(((const Student *)a)->name, ((const Student *)b)->name);
}

/* ================= visitors ================= */

static void print_int(const void *e, void *ctx)
{
    (void)ctx;
    printf("%d ", *(const int *)e);
}

static void sum_int(const void *e, void *ctx)
{
    *(long *)ctx += *(const int *)e;
}

static void print_student(const void *e, void *ctx)
{
    (void)ctx;
    const Student *s = e;
    printf("    %-10s %3d\n", s->name, s->score);
}

/* ================= dispatch table ================= */

static int op_add(int a, int b) { return a + b; }
static int op_sub(int a, int b) { return a - b; }
static int op_mul(int a, int b) { return a * b; }

typedef struct {
    const char *name;
    int (*fn)(int, int);
    const char *help;
} Command;

static const Command commands[] = {
    { "add", op_add, "a + b" },
    { "sub", op_sub, "a - b" },
    { "mul", op_mul, "a * b" }
};
#define COMMAND_COUNT (sizeof commands / sizeof commands[0])

/* ================= polymorphism ================= */

typedef struct Shape {
    const char *name;
    double (*area)(const struct Shape *);
} Shape;

typedef struct { Shape base; double radius; }        Circle;
typedef struct { Shape base; double w, h; }          Rect;

static double circle_area(const Shape *s)
{
    return 3.14159265358979 * ((const Circle *)s)->radius
                            * ((const Circle *)s)->radius;
}
static double rect_area(const Shape *s)
{
    const Rect *r = (const Rect *)s;
    return r->w * r->h;
}

/* ================= variadic ================= */

static int sum_all(int count, ...)
{
    va_list args;
    va_start(args, count);
    int total = 0;
    for (int i = 0; i < count; i++) {
        total += va_arg(args, int);
    }
    va_end(args);
    return total;
}

/* A sentinel-terminated variadic function: no count needed. */
static void print_all(const char *first, ...)
{
    va_list args;
    va_start(args, first);
    for (const char *s = first; s != NULL; s = va_arg(args, const char *)) {
        printf("[%s] ", s);
    }
    va_end(args);
    putchar('\n');
}

static void log_message(const char *fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    fprintf(stderr, "log: ");
    vfprintf(stderr, fmt, args);     /* the v-variant takes a va_list */
    va_end(args);
}

/* ================= _Generic ================= */

#define type_name(x) _Generic((x),   \
    int:          "int",             \
    long:         "long",            \
    double:       "double",          \
    char *:       "char *",          \
    const char *: "const char *",    \
    default:      "other")

int main(void)
{
    puts("== one implementation, three element types ==");

    Vector ints;
    vector_init(&ints, sizeof(int), 4);
    int sample[] = { 42, 7, 99, 13, 5, 77 };
    for (size_t i = 0; i < 6; i++) vector_push(&ints, &sample[i]);

    printf("  unsorted: ");  vector_each(&ints, print_int, NULL);
    vector_sort(&ints, cmp_int);
    printf("\n  sorted:   ");  vector_each(&ints, print_int, NULL);

    long total = 0;
    vector_each(&ints, sum_int, &total);
    printf("\n  sum via a visitor with context: %ld\n", total);

    int key = 99;
    int *found = bsearch(&key, ints.data, ints.count, sizeof(int), cmp_int);
    printf("  bsearch 99: %s\n", found ? "found" : "not found");

    Vector doubles;
    vector_init(&doubles, sizeof(double), 4);
    double d[] = { 2.5, 1.0, 3.75 };
    for (size_t i = 0; i < 3; i++) vector_push(&doubles, &d[i]);
    vector_sort(&doubles, cmp_double);
    printf("  doubles sorted: %g %g %g\n",
           *(double *)vector_at(&doubles, 0),
           *(double *)vector_at(&doubles, 1),
           *(double *)vector_at(&doubles, 2));

    Vector students;
    vector_init(&students, sizeof(Student), 4);
    Student roster[] = {
        { "Ada", 95 }, { "Dennis", 88 }, { "Ken", 91 }, { "Grace", 78 }
    };
    for (size_t i = 0; i < 4; i++) vector_push(&students, &roster[i]);

    puts("  students by score:");
    vector_sort(&students, cmp_student_score);
    vector_each(&students, print_student, NULL);
    puts("  students by name:");
    vector_sort(&students, cmp_student_name);
    vector_each(&students, print_student, NULL);
    puts("  the same sort and the same container, three element types");

    puts("\n== dispatch table ==");
    for (size_t i = 0; i < COMMAND_COUNT; i++) {
        printf("  %-4s (%s): %d\n", commands[i].name, commands[i].help,
               commands[i].fn(10, 3));
    }

    puts("\n== polymorphism through a function pointer ==");
    Circle c = { { "circle", circle_area }, 2.0 };
    Rect   r = { { "rect",   rect_area   }, 3.0, 4.0 };
    Shape *shapes[] = { (Shape *)&c, (Shape *)&r };
    for (size_t i = 0; i < 2; i++) {
        printf("  %-7s area = %.4f\n", shapes[i]->name,
               shapes[i]->area(shapes[i]));
    }

    puts("\n== variadic functions ==");
    printf("  sum_all(3, 1,2,3)       = %d\n", sum_all(3, 1, 2, 3));
    printf("  sum_all(5, 10,20,30,40,50) = %d\n",
           sum_all(5, 10, 20, 30, 40, 50));
    printf("  sentinel-terminated: ");
    print_all("alpha", "beta", "gamma", NULL);
    log_message("a wrapped printf, value = %d\n", 42);

    puts("\n== _Generic ==");
    int i_val = 1; double d_val = 1.0; const char *s_val = "x";
    printf("  1     is %s\n", type_name(i_val));
    printf("  1.0   is %s\n", type_name(d_val));
    printf("  \"x\"   is %s\n", type_name(s_val));

    vector_destroy(&ints);
    vector_destroy(&doubles);
    vector_destroy(&students);
    return EXIT_SUCCESS;
}
gcc -std=c17 -Wall -Wextra -g -fsanitize=address -o generic generic.c -lm
./generic

Demonstrate the comparator overflow

Replace cmp_int with the textbook version and sort a hostile array:

static int cmp_int_broken(const void *a, const void *b)
{
    return *(const int *)a - *(const int *)b;     /* overflows */
}

int hostile[] = { INT_MAX, -1, 0, INT_MIN, 5 };
qsort(hostile, 5, sizeof hostile[0], cmp_int_broken);

INT_MAX - (-1) overflows and the sign is wrong, so the sort concludes that INT_MAX is smaller than −1. Build with -fsanitize=undefined and it is reported:

runtime error: signed integer overflow: 2147483647 - -1 cannot be
represented in type 'int'

Swap in the subtraction-free form and the same array sorts correctly. This is one of the most widespread bugs in C code that otherwise looks textbook-perfect.

See that the type system is genuinely gone

double wrong = 3.14;
vector_push(&ints, &wrong);      /* compiles without a murmur */

The vector copies sizeof(int) bytes out of a double and stores the result as an integer. No warning, no error — the price of void *. This is exactly the check a C++ template or a Rust generic would perform, and it is what the macro-based alternative buys back.

Confirm the variadic promotion rule

static double average(int count, ...)
{
    va_list args;
    va_start(args, count);
    double total = 0;
    for (int i = 0; i < count; i++) {
        total += va_arg(args, float);    /* WRONG: always promoted to double */
    }
    va_end(args);
    return total / count;
}

Call it with average(2, 1.5f, 2.5f) and the result is nonsense: the arguments were promoted to double and eight bytes were pushed, while va_arg reads four. Change float to double and it is correct.

8Common mistakes

MistakeWhat happensFix
return x - y; in a comparatorOverflow; wrong sort order(x > y) - (x < y).
int *f(void) when a function pointer was meantDeclares a function returning a pointerint (*f)(void) — parentheses.
Passing the key by value to bsearchCompile error or wrong resultsPass its address.
bsearch on an unsorted arraySilently wrong resultsSort with the same comparator first.
Pointer arithmetic on void *Not standard CCast to char *.
va_arg(args, float)Reads four bytes of an eight-byte valueUse double; likewise int for char.
Missing va_endUndefined behavior on some ABIsAlways call it before returning.
Base struct not first in a "derived" typeThe upcast points at the wrong memberPut it first.

9Check yourself

Why must a comparator avoid return x - y;?

Because the subtraction can overflow — INT_MAX - (-1) is undefined behavior — and when it wraps, the sign is wrong, so the sort concludes the larger value is smaller. The result is a silently mis-ordered array. (x > y) - (x < y) computes the same three-way answer without arithmetic that can overflow.

How does qsort sort elements of a type it knows nothing about?

It is told everything it needs: the base address, the element count, the size of one element so it can compute addresses and swap bytes, and a comparator that can order any two. All type-specific knowledge lives in the comparator you supply, which casts the const void * arguments back to the real type.

Why cast to char * before doing arithmetic on a void *?

Because arithmetic on void * is not standard C — the element size is unknown, so the scaling is undefined. GCC allows it as an extension treating it as bytes, but portable code converts to char *, whose element size the standard fixes at 1, making byte arithmetic explicit and correct everywhere.

Why can a variadic function not check its arguments?

Because nothing about the extra arguments survives to run time — no count, no types. The function must be told separately, through an explicit count, a sentinel value, or a format string. va_arg simply reads the number of bytes for the type you name, so a wrong type reads the wrong bytes. This is precisely why a mismatched printf specifier is undefined behavior.

Why must the base structure be the first member when simulating inheritance?

Because the standard guarantees that a pointer to a structure, suitably converted, points to its first member and vice versa. With the base first, a Circle * and a Shape * hold the same address, so casting between them is well defined. Put another member first and the cast silently yields a pointer to the wrong bytes.

10Where this leads

Week 31 applies the callback idea to the outside of the program: parsing command-line options, layering configuration from files and the environment, and designing an interface a shell script can rely on. Then weeks 32 and 33 use the generic container you just built as the foundation for linked structures and an abstract data type.