void* y type erasure
void* es el puntero genérico a objeto de C. Puede contener la dirección de cualquier
objeto, pero no recuerda el tipo, tamaño, ownership ni lifetime del objeto. Eso es type
erasure: el programa conserva "dónde" mientras descarta "qué." Así APIs como malloc,
memcpy, qsort, allocators y contextos de callback se mantienen genéricas en C. El
poder es real, y la factura también: cada puntero borrado tiene que viajar con suficiente
información externa para recuperar su significado de forma segura.
El reset:
void*significa "hay algún objeto ahí." No significa "puedo dereferenciar esto." Antes de leer o escribir, tenés que elegir el tipo destino correcto o tratar el storage explícitamente como bytes.
El contrato del puntero borrado
void es un tipo incompleto sin tamaño de objeto, así que un void* no puede
dereferenciarse en C portable y la aritmética sobre él no está definida por el lenguaje.
Podés convertir punteros a objeto a void* y volver:
int x = 42;
void *erased = &x;
int *restored = erased;
El puntero restaurado sirve solo si el tipo original, la alineación y el lifetime siguen
siendo ciertos. void* no transporta esos hechos. Por eso las buenas APIs genéricas pasan
los hechos faltantes al lado del puntero borrado:
| Forma de API | Hecho faltante provisto afuera |
|---|---|
malloc(size_t bytes) |
el caller elige tipo destino y count |
memcpy(void *dst, const void *src, size_t n) |
el caller provee byte count |
qsort(void *base, size_t count, size_t size, cmp) |
el caller provee element count, element size y comparator |
callbacks con void *ctx |
caller y callee acuerdan el tipo del contexto |
Hay un camino especial a nivel byte: la representación de cualquier objeto puede
inspeccionarse a través de un character type, comúnmente unsigned char*. Usá void*
para borrar tipo de objeto; usá unsigned char* cuando querés bytes de verdad.
Cómo funciona realmente
En runtime, void* es solo un valor de puntero. El compilador deja de conocer el tipo del
objeto apuntado en esa expresión. Eso vuelve ilegales o imposibles las operaciones que
requieren información de tipo:
void *p = ...;
/* *p; // inválido: ¿qué tamaño y tipo debería leerse? */
/* p + 1; // no es C portable: void no tiene tamaño */
La información de tipo se mueve al contrato de la API. qsort recibe un void *base, un
count, un element size y un comparator. Cuando se llama al comparator, sus dos argumentos
son punteros const void* a elementos. El comparator debe castearlos de vuelta al tipo
real antes de leer:
static int compare_ints(const void *left, const void *right) {
const int *a = left;
const int *b = right;
return (*a > *b) - (*a < *b);
}
El lado del allocator es parecido. malloc devuelve void* porque provee storage crudo,
no un objeto tipado. La asignación a int *, struct Node * u otro puntero a objeto es
donde el caller elige la interpretación. Por eso la expresión de tamaño tiene que coincidir
con el tipo de puntero final: malloc(count * sizeof *items).
No extiendas esta regla a function pointers. C separa object pointers y function pointers.
Un void* es un puntero genérico a objeto, no un contenedor portable para un puntero a
función. Muchas máquinas representan ambos como direcciones, pero el lenguaje no promete
que las conversiones de punteros a objeto apliquen a punteros a código.
Artefacto ejecutable: borrar, restaurar, ordenar
El demo vive en examples/pointers-and-memory/void-star-and-type-erasure/demo.c.
// demo.c — muestra `void*` como transporte de direcciones: vistas borradas,
// callback de `qsort`, dump de bytes y copia con `memcpy`.
// Compila limpio y corre con:
//
// gcc -O0 -Wall -Wextra demo.c -o demo && ./demo
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct AnyView {
const void *data;
size_t size;
void (*print)(const void *data);
};
static void print_int(const void *data) {
const int *value = data;
printf("int view = %d\n", *value);
}
static void print_c_string(const void *data) {
const char *const *text = data;
printf("string view = %s\n", *text);
}
static int compare_ints(const void *left, const void *right) {
const int *a = left;
const int *b = right;
return (*a > *b) - (*a < *b);
}
static void dump_bytes(const void *data, size_t size) {
const unsigned char *bytes = data;
printf("raw bytes =");
for (size_t i = 0; i < size; i++) {
printf(" %02x", bytes[i]);
}
printf("\n");
}
int main(void) {
int number = 0x12345678;
const char *word = "atlas";
struct AnyView views[] = {
{&number, sizeof number, print_int},
{&word, sizeof word, print_c_string},
};
for (size_t i = 0; i < sizeof views / sizeof views[0]; i++) {
views[i].print(views[i].data);
}
dump_bytes(&number, sizeof number);
int values[] = {40, 10, 30, 20};
qsort(values, sizeof values / sizeof values[0], sizeof values[0], compare_ints);
printf("qsort result =");
for (size_t i = 0; i < sizeof values / sizeof values[0]; i++) {
printf(" %d", values[i]);
}
printf("\n");
void *erased = malloc(sizeof number);
if (erased == NULL) {
perror("malloc");
return 1;
}
memcpy(erased, &number, sizeof number);
int *restored = erased;
printf("restored from void* = 0x%x\n", *restored);
free(erased);
return 0;
}
Compilá y corré:
gcc -O0 -Wall -Wextra demo.c -o demo
./demo
Salida real:
int view = 305419896
string view = atlas
raw bytes = 78 56 34 12
qsort result = 10 20 30 40
restored from void* = 0x12345678
AnyView empareja un puntero borrado con una función de impresión que sabe recuperar el
tipo. dump_bytes cambia deliberadamente de void* a unsigned char*, así que el objeto
ahora se mira como bytes. qsort usa void* más element size y comparator para ordenar
sin conocer el tipo del elemento.
Modos de falla y trade-offs
- Tamaño faltante. Un
void*solo no sabe cuánto storage es válido. Las APIs genéricas necesitan unsize_tcount, byte count o centinela. - Cast equivocado al restaurar. Castear
void*al tipo de puntero incorrecto y dereferenciar puede violar alineación, effective type o reglas de lifetime. - Asumir aritmética de bytes. GNU C acepta aritmética sobre
void*como extensión. C portable no. Casteá aunsigned char*para caminar bytes. - Perder
const. Unconst void *preserva acceso read-only. Castearlo avoid *y escribir rompe la promesa y puede ser undefined behavior si el objeto original es const. - Borrar ownership.
void*no dice quién libera el objeto. Contextos de callback y contenedores deben documentar si prestan o poseen. - Confundir function pointers. No guardes function pointers en
void*salvo que una API específica de plataforma lo garantice explícitamente.
En la práctica
- Pasá puntero borrado más metadata. APIs con type erasure deberían llevar tamaño, count, supuestos de alineación y reglas de ownership en parámetros o un wrapper struct.
- Usá
const void *para input genérico read-only. Comunica que el callee puede inspeccionar pero no mutar el objeto apuntado. - Usá
unsigned char *para bytes crudos. Es la herramienta portable de inspección y copia byte-a-byte. - Mantené los casts cerca de la frontera. Casteá una vez al borde del callback o función genérica, después usá punteros tipados adentro.
- Preferí wrappers tipados chicos para patrones repetidos de
void*. UnBuffer,SliceoAnyViewvuelve visibles los contratos.
Conecta con: Qué es realmente un puntero · Aritmética de punteros y stride · El heap: malloc/free y el allocator por debajo · Function pointers · C desde el Metal
Fuentes
- ISO/IEC 9899 (working drafts del estándar C en WG14) — conversiones de object pointers, acceso con character types a representaciones de objetos,
voidy límites de undefined behavior. https://www.open-std.org/jtc1/sc22/wg14/ - cppreference — Pointer declaration —
void*, conversiones de object pointers, null pointers y sintaxis de punteros multinivel. https://en.cppreference.com/w/c/language/pointer - cppreference —
qsort— ejemplo canónico de biblioteca estándar de sorting con type erasure víavoid*y comparator callback. https://en.cppreference.com/w/c/algorithm/qsort - cppreference —
memcpy— copia orientada a bytes con parámetrosvoid*y byte counts explícitos. https://en.cppreference.com/w/c/string/byte/memcpy - Jens Gustedt — Modern C — tratamiento moderno de APIs genéricas en C, representaciones de objetos y disciplina de punteros. https://gustedt.gitlabpages.inria.fr/modern-c/
- Richard Reese — Understanding and Using C Pointers — discusión práctica de
void*, callbacks y contenedores genéricos en C. https://www.oreilly.com/library/view/understanding-and-using/9781449344535/