Genericidad manual: void*, callbacks y macros en C

Cuando programas en C, la genericidad no es una característica integrada del lenguaje, sino una técnica de ingeniería que aplicas para lograr que una misma lógica opere sobre distintos tipos de datos. Antes de la introducción de _Generic en [C11], teníamos dos caminos principales: el borrado de tipos (type erasure) mediante void* o la generación de código mediante el preprocesador.

El uso de void* permite que una función acepte un puntero a “cualquier cosa”, lo que efectivamente le dice al compilador: “no te preocupes por el tipo de dato, yo me encargo”. Esto es lo que permite que funciones como qsort [POSIX] o bsearch [ISO/IEC 9899:2011] funcionen con estructuras complejas. Sin embargo, este poder tiene un coste: el compilador pierde la capacidad de verificar si lo que estás pasando es realmente lo que la función espera. La lógica de la operación (por ejemplo, cómo comparar dos elementos) debe ser delegada al programador mediante un callback (un puntero a función), que es donde reside el conocimiento real del tipo.

Por otro lado, las macros de preprocesador permiten la “especialización” del código. En lugar de usar void* y perder la seguridad de tipos, usamos macros para duplicar la estructura y las funciones para cada tipo específico, generando código que es totalmente type-safe pero que aumenta el tamaño del binario.

Si decides usar void* y cometes un error al castear un puntero, el compilador no te detendrá; simplemente obtendrás una corrupción de memoria o un segmentation fault en tiempo de ejecución. Si usas macros de forma descuidada, el error será difícil de rastrear debido a los nombres de tipos generados por el preprocesador.

#include <stdio.h>
#include <stdlib.h>

/* 
 * 1. Enfoque de "Type Erasure" (void*):
 * Un contenedor que almacena punteros a cualquier objeto.
 * El conocimiento del tipo se mantiene fuera del contenedor.
 */
typedef struct {
    void **items;
    size_t size;
    size_t capacity;
} GenericStack;

void generic_stack_init(GenericStack *s, size_t cap) {
    s->items = malloc(cap * sizeof(void *));
    if (!s->items) exit(EXIT_FAILURE);
    s->size = 0;
    s->capacity = cap;
}

void generic_stack_push(GenericStack *s, void *item) {
    if (s->size < s->capacity) {
        s->items[s->size++] = item;
    }
}

void* generic_stack_pop(GenericStack *s) {
    return (s->size > 0) ? s->items[--s->size] : NULL;
}

void generic_stack_free(GenericStack *s) {
    free(s->items);
}

/* 
 * 2. Enfoque de "Code Generation" (Macros):
 * Creamos un stack que almacena los valores directamente (inline),
 * manteniendo la seguridad de tipos y evitando la indirección de punteros.
 */
#define DEFINE_STACK(T, Name) \
    typedef struct {           \
        T *data;               \
        size_t size;           \
        size_t capacity;       \
    } Name;                      \
    void Name##_push(Name *s, T item); \
    T Name##_pop(Name *s);

// Implementación manual de las funciones generadas para IntStack
// En una biblioteca real, esto se automatizaría con macros más complejas.
void IntStack_push(IntStack *s, int item) {
    if (s->size < s->capacity) s->data[s->size++] = item;
}

int IntStack_pop(IntStack *s) {
    return (s->size > 0) ? s->data[--s->size] : 0;
}

/*
 * 3. Callback para qsort:
 * El algoritmo no sabe qué es un int, solo sabe llamar a esta función.
 */
int compare_ints(const void *a, const void *b) {
    return (*(int*)a - *(int*)b);
}

int main(void) {
    // --- Uso de GenericStack (void*) ---
    // Almacena punteros a enteros. Requiere gestión manual de la memoria de los ints.
    GenericStack g_stack;
    generic_stack_init(&g_stack, 5);

    int *val1 = malloc(sizeof(int)); *val1 = 42;
    int *val2 = malloc(sizeof(int)); *val2 = 7;

    generic_stack_push(&g_stack, val1);
    generic_stack_push(&g_stack, val2);

    printf("GenericStack (void*): ");
    while (g_stack.size > 0) {
        int *v = (int*)generic_stack_pop(&g_stack);
        printf("%d ", *v);
        free(v); // Obligatorio: el stack solo guarda el puntero, no el dato
    }
    printf("\n");
    generic_stack_free(&g_stack);

    // --- Uso de IntStack (Macros) ---
    // Almacena los enteros directamente en el array. Más eficiente.
    IntStack i_stack;
    i_stack.data = malloc(5 * sizeof(int));
    i_stack.size = 0;
    i_stack.capacity = 5;

    i_stack_push(&i_stack, 100);
    i_stack_push(&i_stack, 200);

    printf("IntStack (Macro): %d, %d\n", IntStack_pop(&i_stack), IntStack_pop(&i_stack));
    free(i_stack.data);

    // --- Uso de qsort (Callback) ---
    int array[] = {50, 10, 40, 20, 30};
    qsort(array, 5, sizeof(int), compare_ints);
    
    printf("Array ordenado (qsort): ");
    for (int i = 0; i < 5; i++) printf("%d ", array[i]);
    printf("\n");

    return 0;
}

Desglose del ejemplo

En el GenericStack, el campo items es un void **. Esto significa que estamos almacenando un array de punteros. Fíjate en main: al usar generic_stack_push, pasamos val1 (un int *). El stack no sabe que lo que hay detrás de ese puntero es un entero; solo guarda la dirección. Por eso, al hacer el pop, es imperativo hacer un cast manual (int*) y, lo más importante, el programador debe recordar que debe liberar la memoria con free(v), porque el contenedor no tiene el conocimiento suficiente para saber cómo destruir el objeto.

El IntStack es un ejemplo de generación de código. Gracias a la macro DEFINE_STACK, el compilador ve una estructura IntStack con un campo int *data. A diferencia de la versión genérica, aquí los enteros están “in-line” en el array. No hay punteros extra, lo que mejora la localidad de caché y reduce la fragmentación de memoria.

Finalmente, compare_ints es el puente de conocimiento. qsort es una función genérica que opera sobre void *. Para poder comparar, la función debe “recuperar” el tipo mediante un cast: *(int*)a. Sin este callback, qsort no tendría forma de saber cómo interpretar los bytes de la memoria.

El error frecuente

Un error clásico al usar la técnica de void* es el dangling pointer (puntero colgante) debido a una mala gestión de la visibilidad de las variables.

// ERROR CRÍTICO
void populate_stack(GenericStack *s) {
    int local_val = 10; 
    generic_stack_push(s, &local_val); // ERROR: Pasas la dirección de una variable local
} 

// ... más tarde ...
int *v = (int*)generic_stack_pop(s);
printf("%d", *v); // Comportamiento indefinido: local_val ya no existe en el stack.

Aquí, generic_stack_push guarda una dirección de memoria que pertenece al frame de la función populate_stack. Cuando la función termina, esa dirección ya no es válida. AddressSanitizer (ASan) detectaría esto inmediatamente como un “stack-use-after-scope”. En el enfoque de macros (IntStack), este error es imposible porque el valor se copia por valor al array, no se guarda su dirección.

102

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio