Void Pointers & Generic C Programming: Type-Erased Containers and Manual Polymorphism

Void Pointers & Generic C Programming: Type-Erased Containers and Manual Polymorphism
Table of Contents
- What Makes void* Special?
- Implicit and Explicit Conversions
- Standard Library Generic Functions
- Building Generic Containers with void*
- The size_t + void* Contract
- Type Safety Wrappers: Macro-Based Generics
- C11 _Generic: Selection-Time Dispatch
- Tagged Unions: Type-Aware Storage
- The Linux Kernel's Generic Driver Interface
- Frequently Asked Questions
- Key Takeaway
What Makes void* Special?
Every pointer type in C carries two pieces of information: the address and the type it points to. The type tells the compiler how many bytes constitute one element (sizeof) and enables type-safe arithmetic and dereference.
void* discards the type information, keeping only the address. This is called type erasure. The trade-off:
- ✅ Can accept a pointer to any type without casting.
- ✅ Can store any address in a single container.
- ❌ Cannot be dereferenced directly — you must cast first.
- ❌ Cannot perform pointer arithmetic directly (no element size).
- ❌ Compiler cannot warn if you cast to the wrong type.
Implicit and Explicit Conversions
In C (unlike C++), any data pointer can be assigned to and from void* without an explicit cast:
[!IMPORTANT] In C++, you must explicitly cast
void*to any other pointer type. This is why C++ code often uses(int*)malloc(...)— the cast is mandatory for C++ compilation. Pure C code should omit the cast (it can hide the bug wheremallocis not included and the compiler silently treats it as returning int).
Standard Library Generic Functions
The C standard library was designed around void* for exactly this purpose — enabling functions that work on any data type:
memcpy: Copy Any Data
[!WARNING]
memcpyvsmemmove:memcpyassumes source and destination do not overlap. If they overlap, usememmove— it handles overlapping regions correctly at a slight performance cost.
qsort: Sort Any Array
bsearch: Binary Search Any Sorted Array
Building Generic Containers with void*
A generic stack that can hold any type:
The key insight: (char*)s->data + i * s->elem_size turns byte-level pointer arithmetic into element-level access. The char* cast enables single-byte arithmetic.
The size_t + void* Contract
Every generic function in C follows the same two-parameter contract: a void* for the data and a size_t for the element size. This is the information minimum needed to operate on any type:
Type Safety Wrappers: Macro-Based Generics
Pure void* APIs lose all type safety. Macros can restore it at compile time:
This pattern generates type-safe, zero-overhead wrappers per type — similar to C++ templates but done at the preprocessor level.
C11 _Generic: Selection-Time Dispatch
C11 introduced _Generic for compile-time type dispatching without preprocessor abuse:
_Generic is used internally by C11's <tgmath.h> to provide type-generic math functions. It enables zero-overhead type dispatch that is resolved entirely at compile time.
Frequently Asked Questions
Why can't I dereference a void pointer directly?
Dereferencing a pointer means loading sizeof(*ptr) bytes from the address. For void*, there is no type, so sizeof(*void_ptr) is meaningless (undefined in standard C). The compiler needs to know the element size to perform safe dereference. You must cast to a concrete pointer type first.
Is it safe to cast between unrelated pointer types?
Casting between unrelated types and dereferencing the result is generally undefined behavior (violates strict aliasing rules). The only safe casts are: any pointer ↔ void*, any pointer ↔ char* (for byte access), and reading back through the exact same type originally written. For type punning (reinterpreting bits), use memcpy to a union or intermediate buffer.
What is strict aliasing and how does it relate to void?*
The strict aliasing rule says the compiler may assume that pointers of different types never alias (point to the same memory). This allows aggressive optimizations. Breaking strict aliasing (e.g., casting int* to float* and dereferencing) is undefined behavior. char* and void* are exceptions — they can alias anything.
How does malloc return a void that works for any type?*
malloc simply asks the OS for a block of untyped memory and returns its start address as void*. The caller provides meaning by casting to the desired type. C's type system enforces safe usage after the cast — but only if you cast to the correct type.
Key Takeaway
void* is C's mechanism for intentional type erasure — deliberately forgetting type information to enable universal container and algorithm code. Used carefully — always alongside size_t for element size, type tags for heterogeneous storage, and macro wrappers for restore type safety — it provides genuine generic programming with zero runtime overhead.
Read next: The C Preprocessor: Macros, Conditional Compilation & Code Generation →
Part of the C Mastery Course — 30 modules from foundations to expert-level systems programming.
