Generic Programming: void* to Generics
C has no generics in the language. Instead it has three escape hatches:
void*, macros, and _Generic. This chapter shows all three, then shows how
Rust does it properly with monomorphized generics and trait bounds.
The void* Pattern
void* is C's universal pointer -- it can point to any type. The cost is
total loss of type information. The compiler cannot check that you cast
correctly.
/* void_swap.c -- generic swap with void* */
#include <stdio.h>
#include <string.h>
void generic_swap(void *a, void *b, size_t size)
{
unsigned char tmp[size]; /* VLA -- C99 */
memcpy(tmp, a, size);
memcpy(a, b, size);
memcpy(b, tmp, size);
}
int main(void)
{
int x = 10, y = 20;
generic_swap(&x, &y, sizeof(int));
printf("x=%d y=%d\n", x, y); /* x=20 y=10 */
double p = 3.14, q = 2.72;
generic_swap(&p, &q, sizeof(double));
printf("p=%.2f q=%.2f\n", p, q); /* p=2.72 q=3.14 */
return 0;
}
This works, but nothing stops you from passing the wrong size or casting the
result to the wrong type. The bug compiles cleanly and corrupts memory at
runtime.
Caution:
void*erases the type. The compiler will not warn if you passsizeof(int)for adouble*. The resulting memory corruption is silent and undefined.
qsort: The Classic void* API
The C standard library's qsort is the canonical example. Its signature:
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
Every argument is void* or size_t. The comparison function receives void*
and must cast to the real type.
/* qsort_demo.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int cmp_int(const void *a, const void *b)
{
int ia = *(const int *)a;
int ib = *(const int *)b;
return (ia > ib) - (ia < ib);
}
int cmp_str(const void *a, const void *b)
{
const char *sa = *(const char **)a;
const char *sb = *(const char **)b;
return strcmp(sa, sb);
}
int main(void)
{
int nums[] = {5, 3, 8, 1, 4};
qsort(nums, 5, sizeof(int), cmp_int);
for (int i = 0; i < 5; i++)
printf("%d ", nums[i]);
printf("\n"); /* 1 3 4 5 8 */
const char *words[] = {"cherry", "apple", "banana"};
qsort(words, 3, sizeof(char *), cmp_str);
for (int i = 0; i < 3; i++)
printf("%s ", words[i]);
printf("\n"); /* apple banana cherry */
return 0;
}
Try It: Write a
cmp_int_desccomparator that sorts integers in descending order. Pass it toqsortand verify the output.
typedef for Clarity
Raw function-pointer types are hard to read. typedef helps.
/* typedef_demo.c */
#include <stdio.h>
/* Without typedef: hard to parse */
int (*get_operation_raw(char op))(int, int);
/* With typedef: much clearer */
typedef int (*binop_fn)(int, int);
int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
binop_fn get_operation(char op)
{
switch (op) {
case '+': return add;
case '*': return mul;
default: return NULL;
}
}
int main(void)
{
binop_fn f = get_operation('+');
if (f)
printf("3 + 4 = %d\n", f(3, 4)); /* 7 */
return 0;
}
Macro-Based Generics
When void* is too dangerous, C programmers reach for macros. The preprocessor
does text substitution before the compiler sees the code, so a macro can
"generate" type-specific functions.
/* macro_generic.c -- type-safe min/max via macros */
#include <stdio.h>
#define DEFINE_MIN(TYPE, NAME) \
static inline TYPE NAME(TYPE a, TYPE b) { return a < b ? a : b; }
DEFINE_MIN(int, min_int)
DEFINE_MIN(double, min_double)
int main(void)
{
printf("min_int(3,7) = %d\n", min_int(3, 7));
printf("min_double(3.1,2.7) = %f\n", min_double(3.1, 2.7));
return 0;
}
The kernel takes this further. list_for_each_entry iterates over an
intrusive list and hands you typed pointers -- no casting needed.
/* Simplified kernel-style iteration macro */
#include <stdio.h>
#include <stddef.h>
#define container_of(ptr, type, member) \
((type *)((char *)(ptr) - offsetof(type, member)))
struct list_head {
struct list_head *next;
struct list_head *prev;
};
/* Initialize a list head to point to itself (empty circular list) */
#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) struct list_head name = LIST_HEAD_INIT(name)
static inline void list_add(struct list_head *new_node,
struct list_head *head)
{
new_node->next = head->next;
new_node->prev = head;
head->next->prev = new_node;
head->next = new_node;
}
#define list_for_each_entry(pos, head, member) \
for (pos = container_of((head)->next, typeof(*pos), member); \
&pos->member != (head); \
pos = container_of(pos->member.next, typeof(*pos), member))
struct task {
int pid;
struct list_head link;
};
int main(void)
{
LIST_HEAD(tasks);
struct task t1 = { .pid = 1 };
struct task t2 = { .pid = 2 };
struct task t3 = { .pid = 3 };
list_add(&t1.link, &tasks);
list_add(&t2.link, &tasks);
list_add(&t3.link, &tasks);
struct task *pos;
list_for_each_entry(pos, &tasks, link)
printf("pid = %d\n", pos->pid);
return 0;
}
Circular doubly linked list after adding t1, t2, t3:
tasks (sentinel)
|
+--next--> t3.link --next--> t2.link --next--> t1.link --+
+--prev--- t1.link <--prev-- t2.link <--prev-- t3.link <-+
Driver Prep:
list_for_each_entryandcontainer_ofare the two macros you will use most often in kernel code. Master them now.
_Generic in C11
C11 introduced _Generic, a compile-time type dispatch. It selects an
expression based on the type of its controlling argument.
/* generic_c11.c -- _Generic dispatch (requires C11) */
#include <stdio.h>
#include <math.h>
#define abs_val(x) _Generic((x), \
int: abs, \
long: labs, \
float: fabsf, \
double: fabs \
)(x)
#define print_val(x) _Generic((x), \
int: print_int, \
double: print_double, \
char *: print_str \
)(x)
void print_int(int x) { printf("int: %d\n", x); }
void print_double(double x) { printf("double: %.2f\n", x); }
void print_str(char *s) { printf("string: %s\n", s); }
int main(void)
{
printf("abs(-3) = %d\n", abs_val(-3));
printf("abs(-3.5) = %.1f\n", abs_val(-3.5));
print_val(42);
print_val(3.14);
print_val("hello");
return 0;
}
_Generic is limited: it dispatches on a fixed set of types you enumerate.
You cannot write open-ended generic code with it. It is best used for
type-overloaded convenience macros.
The typedef + Function Pointer + Macro Trinity
In practice, "generic" C code combines all three techniques:
- typedef names the function-pointer type so humans can read it.
- Function pointers supply type-specific operations at runtime.
- Macros stamp out boilerplate and provide type-safe wrappers.
/* trinity.c -- combining typedef, fn pointers, and macros */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 1. typedef for clarity */
typedef int (*cmp_fn)(const void *, const void *);
/* 2. "Generic" sorted array with function pointer for comparison */
struct sorted_array {
void *data;
size_t elem_size;
size_t len;
size_t cap;
cmp_fn cmp;
};
struct sorted_array sa_new(size_t elem_size, size_t cap, cmp_fn cmp)
{
struct sorted_array sa;
sa.data = malloc(elem_size * cap);
sa.elem_size = elem_size;
sa.len = 0;
sa.cap = cap;
sa.cmp = cmp;
return sa;
}
void sa_insert(struct sorted_array *sa, const void *elem)
{
if (sa->len >= sa->cap) {
fprintf(stderr, "sorted_array full\n");
return;
}
/* Find insertion point (linear scan for simplicity) */
size_t i;
char *base = (char *)sa->data;
for (i = 0; i < sa->len; i++) {
if (sa->cmp(elem, base + i * sa->elem_size) < 0)
break;
}
/* Shift elements right */
memmove(base + (i + 1) * sa->elem_size,
base + i * sa->elem_size,
(sa->len - i) * sa->elem_size);
memcpy(base + i * sa->elem_size, elem, sa->elem_size);
sa->len++;
}
/* 3. Macro for type-safe access */
#define SA_GET(sa, type, index) (((type *)(sa)->data)[index])
void sa_free(struct sorted_array *sa) { free(sa->data); }
int cmp_int(const void *a, const void *b)
{
int ia = *(const int *)a;
int ib = *(const int *)b;
return (ia > ib) - (ia < ib);
}
int main(void)
{
struct sorted_array sa = sa_new(sizeof(int), 16, cmp_int);
int vals[] = {5, 3, 8, 1, 7};
for (int i = 0; i < 5; i++)
sa_insert(&sa, &vals[i]);
printf("Sorted: ");
for (size_t i = 0; i < sa.len; i++)
printf("%d ", SA_GET(&sa, int, i));
printf("\n"); /* 1 3 5 7 8 */
sa_free(&sa);
return 0;
}
Caution: The
SA_GETmacro trusts you to pass the right type. Get it wrong and you read garbage. C has no defense against this.
Rust: Real Generics
Rust generics are monomorphized -- the compiler generates a separate copy of the function for each concrete type used. You get type safety and zero runtime cost.
// generics.rs -- Rust generics with trait bounds fn min_val<T: PartialOrd>(a: T, b: T) -> T { if a < b { a } else { b } } fn print_sorted<T: Ord + std::fmt::Debug>(mut items: Vec<T>) { items.sort(); println!("{:?}", items); } fn main() { println!("min(3, 7) = {}", min_val(3, 7)); println!("min(3.1, 2.7) = {}", min_val(3.1, 2.7)); print_sorted(vec![5, 3, 8, 1, 7]); // [1, 3, 5, 7, 8] print_sorted(vec!["cherry", "apple", "banana"]); }
Rust Note: Monomorphization means
min_val::<i32>andmin_val::<f64>are two separate compiled functions. There is novoid*, no casting, no runtime dispatch. The compiler catches type errors at compile time.
Trait Bounds and Where Clauses
Trait bounds constrain what a generic type must support. A where clause is
just a cleaner way to write the same constraints.
// trait_bounds.rs use std::fmt::Display; use std::ops::Add; // Inline bounds fn sum_and_print<T: Add<Output = T> + Display + Copy>(a: T, b: T) { let result = a + b; println!("{} + {} = {}", a, b, result); } // Equivalent with where clause (clearer for many bounds) fn describe<T>(item: &T) where T: Display + PartialOrd + Copy, { println!("Value: {}", item); } // Multiple generic parameters fn largest<T>(list: &[T]) -> &T where T: PartialOrd, { let mut max = &list[0]; for item in &list[1..] { if item > max { max = item; } } max } fn main() { sum_and_print(3, 4); // 3 + 4 = 7 sum_and_print(1.5, 2.5); // 1.5 + 2.5 = 4 describe(&42); describe(&"hello"); let nums = vec![5, 3, 8, 1, 7]; println!("largest = {}", largest(&nums)); // 8 }
Generic Structs
In C, you fake generic containers with void* and macros. In Rust, you
declare a generic struct directly.
// generic_struct.rs struct SortedVec<T: Ord> { data: Vec<T>, } impl<T: Ord> SortedVec<T> { fn new() -> Self { SortedVec { data: Vec::new() } } fn insert(&mut self, value: T) { let pos = self.data.binary_search(&value).unwrap_or_else(|e| e); self.data.insert(pos, value); } fn contains(&self, value: &T) -> bool { self.data.binary_search(value).is_ok() } fn iter(&self) -> impl Iterator<Item = &T> { self.data.iter() } } fn main() { let mut sv = SortedVec::new(); sv.insert(5); sv.insert(3); sv.insert(8); sv.insert(1); sv.insert(7); print!("Sorted: "); for v in sv.iter() { print!("{} ", v); } println!(); // 1 3 5 7 8 println!("contains 3? {}", sv.contains(&3)); // true println!("contains 6? {}", sv.contains(&6)); // false }
Why the Kernel Chose Macros Over void*
The kernel avoids void* for linked lists. Here is why:
void* approach: Macro approach:
struct list_node { struct list_head {
void *data; <-- cast! struct list_head *next;
struct list_node *next; struct list_head *prev;
}; };
// Embed list_head in your struct.
Requires: // container_of recovers parent.
- runtime cast on every access
- extra indirection (pointer Requires:
to data, not data itself) - typeof / offsetof at compile time
- no type checking - zero extra indirection
- type-safe iteration macros
The macro approach gives:
- No extra allocations: the list node lives inside the data struct.
- No casts at use sites:
list_for_each_entryhands you a typed pointer. - No double indirection: one fewer pointer dereference per access.
Try It: Rewrite the
generic_swapfunction from the beginning of this chapter using a macro instead ofvoid*. The macro version should work for any type without requiring asizeparameter. (Hint: usetypeof.)
Knowledge Check
- What goes wrong if you pass
sizeof(int)togeneric_swapbut the pointers actually point todoublevalues? - In
_Generic, what happens if the controlling expression's type does not match any of the listed types and there is nodefaultcase? - What does "monomorphization" mean in Rust, and how does it differ from C++'s template instantiation?
Common Pitfalls
- Casting
void*to the wrong type -- the compiler says nothing, the program corrupts memory silently. - Macro hygiene -- macro arguments evaluated multiple times cause bugs:
MIN(x++, y)incrementsxtwice. _Genericdoes not support user-defined types easily -- you must enumerate every type explicitly.- Forgetting trait bounds in Rust -- the compiler will tell you exactly which trait is missing, but the error messages can be long.
- Over-constraining generics -- requiring more traits than necessary reduces reusability.