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.
- Declare, assign, and call a function pointer, and read its declaration.
- Use
qsortandbsearchwith 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.
| Gain | Cost |
|---|---|
| One implementation for every type | No type checking at all |
| Works with any element size | Every access needs a cast |
| Standard, portable | memcpy 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
./genericDemonstrate 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
| Mistake | What happens | Fix |
|---|---|---|
return x - y; in a comparator | Overflow; wrong sort order | (x > y) - (x < y). |
int *f(void) when a function pointer was meant | Declares a function returning a pointer | int (*f)(void) — parentheses. |
Passing the key by value to bsearch | Compile error or wrong results | Pass its address. |
bsearch on an unsorted array | Silently wrong results | Sort with the same comparator first. |
Pointer arithmetic on void * | Not standard C | Cast to char *. |
va_arg(args, float) | Reads four bytes of an eight-byte value | Use double; likewise int for char. |
Missing va_end | Undefined behavior on some ABIs | Always call it before returning. |
| Base struct not first in a "derived" type | The upcast points at the wrong member | Put 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.