A programação genérica em C é uma técnica que permite escrever código que funciona com diferentes tipos de dados, sem duplicação. O principal mecanismo para isso é o ponteiro genérico void*, que pode armazenar o endereço de qualquer tipo de dado. Nesta aula, vamos aprofundar como usar void* para criar estruturas de dados genéricas, como listas e pilhas, e discutir as boas práticas para casting seguro, além dos trade-offs envolvidos.

Dominar essa técnica é essencial para programadores C que desejam escrever bibliotecas reutilizáveis, como as funções qsort e bsearch da biblioteca padrão, que operam sobre qualquer tipo de dado. Vamos entender como o compilador lida com void* e como evitar erros comuns.

Estruturas genéricas

Estruturas genéricas são aquelas que podem armazenar dados de qualquer tipo. Em C, isso é alcançado usando campos do tipo void* para armazenar os dados. Por exemplo, uma lista encadeada genérica pode ser definida assim:

typedef struct Node {
    void* data;
    struct Node* next;
} Node;

typedef struct {
    Node* head;
    size_t size;
} List;

Essa estrutura permite que a lista armazene ponteiros para qualquer tipo de dado. O usuário da lista é responsável por alocar e gerenciar a memória dos dados, bem como por fazer o casting correto ao recuperar os elementos. A função list_insert pode ser implementada de forma genérica:

void list_insert(List* list, void* data) {
    Node* new_node = malloc(sizeof(Node));
    new_node->data = data;
    new_node->next = list->head;
    list->head = new_node;
    list->size++;
}

Um exemplo de uso:

int a = 42;
char* b = "hello";
List list = {0};
list_insert(&list, &a);
list_insert(&list, b);
// Para recuperar:
int* pa = (int*)list.head->data; // mas cuidado: a ordem é inversa

Note que a lista não sabe o tipo dos dados, então é responsabilidade do programador garantir que o casting seja feito corretamente.

Casting seguro

Casting de void* para um tipo específico é feito com um cast explícito: (int*)ptr. No entanto, isso é inerentemente inseguro, pois o compilador não verifica se o ponteiro realmente aponta para um int. Para minimizar riscos, é importante seguir algumas práticas:

  • Documente claramente o tipo esperado em cada função que recebe void*.
  • Use funções de comparação que recebem const void* e fazem o cast interno, como em qsort.
  • Evite casts entre tipos incompatíveis; se necessário, use uniões ou estruturas com tags.
  • Considere usar _Generic (C11) para seleção de tipo em tempo de compilação, embora não substitua void* para armazenamento.

Um exemplo de função de comparação segura:

int compare_int(const void* a, const void* b) {
    int ia = *(const int*)a;
    int ib = *(const int*)b;
    return (ia > ib) - (ia < ib);
}

Essa função pode ser passada para qsort e funciona para qualquer array de ints. O casting é feito dentro da função, e o usuário não precisa se preocupar.

Outra prática é usar um campo que indique o tipo, como em estruturas variantes:

typedef enum { INT, FLOAT, STRING } Type;

typedef struct {
    Type type;
    void* data;
} Value;

Assim, ao recuperar data, você sabe qual cast usar.

Trade-offs

O uso de void* traz vantagens e desvantagens. Entre as vantagens:

  • Reutilização de código: uma única implementação para todos os tipos.
  • Flexibilidade: permite criar bibliotecas como qsort que funcionam com qualquer array.
  • Redução de duplicação.

Desvantagens:

  • Perda de segurança de tipos: o compilador não detecta erros de tipo.
  • Necessidade de casting manual, que pode levar a erros se não for cuidadoso.
  • Dificuldade de depuração: erros de tipo podem se manifestar em tempo de execução.
  • Overhead de desempenho: o acesso indireto pode ser mais lento que tipos concretos, embora geralmente seja mínimo.

Além disso, o gerenciamento de memória se torna mais complexo, pois o código genérico não sabe como copiar ou liberar os dados. Por exemplo, se a lista precisar liberar a memória dos dados, ela não sabe se deve chamar free ou não. Isso geralmente é resolvido com funções de callback fornecidas pelo usuário.

Exemplos

Vamos implementar uma pilha genérica completa com funções de push, pop e destruição, usando callbacks para liberar os dados.

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

typedef struct Stack {
    void** items;
    size_t capacity;
    size_t size;
} Stack;

Stack* stack_create(size_t capacity) {
    Stack* s = malloc(sizeof(Stack));
    s->items = malloc(capacity * sizeof(void*));
    s->capacity = capacity;
    s->size = 0;
    return s;
}

void stack_push(Stack* s, void* item) {
    if (s->size == s->capacity) {
        s->capacity *= 2;
        s->items = realloc(s->items, s->capacity * sizeof(void*));
    }
    s->items[s->size++] = item;
}

void* stack_pop(Stack* s) {
    if (s->size == 0) return NULL;
    return s->items[--s->size];
}

void stack_destroy(Stack* s, void (*free_item)(void*)) {
    for (size_t i = 0; i < s->size; i++) {
        if (free_item) free_item(s->items[i]);
    }
    free(s->items);
    free(s);
}

// Exemplo de uso
int main() {
    Stack* s = stack_create(2);
    int a = 10, b = 20;
    stack_push(s, &a);
    stack_push(s, &b);
    void* item = stack_pop(s);
    printf("%d\n", *(int*)item);
    stack_destroy(s, NULL); // não libera os ints, pois são locais
    return 0;
}

Outro exemplo é usar void* para implementar uma função de busca linear genérica:

int linear_search(const void* key, const void* base, size_t count, size_t size,
                  int (*compare)(const void*, const void*)) {
    const char* arr = base;
    for (size_t i = 0; i < count; i++) {
        if (compare(key, arr + i * size) == 0)
            return i;
    }
    return -1;
}

Essa função funciona com qualquer array, desde que o usuário forneça uma função de comparação.

Boas práticas e observações finais

Ao usar void*, sempre documente o contrato da função: que tipo de dados é esperado, quem aloca e libera memória, e como o casting deve ser feito. Prefira encapsular o acesso aos dados em funções que fazem o cast internamente, em vez de expor void* diretamente.

Em C, a programação genérica com void* é uma ferramenta poderosa, mas deve ser usada com cuidado. Ela é a base de muitas bibliotecas, e dominá-la é um sinal de maturidade na linguagem.

Referências

Exercícios

  1. Implemente uma lista encadeada genérica com funções para inserir no início, remover do início e obter o tamanho. Use void* para os dados. Teste com inteiros e strings.

    ✓ Resposta:
    #include <stdio.h>
    #include <stdlib.h>
    
    typedef struct Node {
        void* data;
        struct Node* next;
    } Node;
    
    typedef struct {
        Node* head;
        size_t size;
    } List;
    
    void list_init(List* l) { l->head = NULL; l->size = 0; }
    void list_push_front(List* l, void* data) {
        Node* n = malloc(sizeof(Node));
        n->data = data;
        n->next = l->head;
        l->head = n;
        l->size++;
    }
    void* list_pop_front(List* l) {
        if (!l->head) return NULL;
        Node* n = l->head;
        l->head = n->next;
        void* data = n->data;
        free(n);
        l->size--;
        return data;
    }
    size_t list_size(List* l) { return l->size; }
    
    int main() {
        List l; list_init(&l);
        int a=1,b=2,c=3;
        list_push_front(&l,&a); list_push_front(&l,&b); list_push_front(&l,&c);
        while (l.size) {
            int* p = (int*)list_pop_front(&l);
            printf("%d ", *p);
        }
        return 0;
    }
    
  2. Escreva uma função de comparação genérica para qsort que ordene um array de estruturas Pessoa (com nome e idade) por idade. Mostre como chamar qsort.

    ✓ Resposta:
    #include <stdio.h>
    #include <stdlib.h>
    
    typedef struct {
        char nome[50];
        int idade;
    } Pessoa;
    
    int cmp_pessoa(const void* a, const void* b) {
        const Pessoa* pa = a;
        const Pessoa* pb = b;
        return pa->idade - pb->idade;
    }
    
    int main() {
        Pessoa p[] = {{"Ana",30},{"Bia",20},{"Carlos",25}};
        size_t n = sizeof(p)/sizeof(p[0]);
        qsort(p, n, sizeof(Pessoa), cmp_pessoa);
        for (size_t i=0; i<n; i++)
            printf("%s %d\n", p[i].nome, p[i].idade);
        return 0;
    }
    
  3. Implemente uma função de busca binária genérica que funcione com qualquer tipo de array e uma função de comparação. Teste com um array de doubles.

    ✓ Resposta:
    #include <stdio.h>
    #include <stdlib.h>
    
    int cmp_double(const void* a, const void* b) {
        double da = *(const double*)a, db = *(const double*)b;
        return (da > db) - (da < db);
    }
    
    void* bsearch_generic(const void* key, const void* base, size_t count, size_t size,
                          int (*cmp)(const void*, const void*)) {
        const char* arr = base;
        size_t left = 0, right = count;
        while (left < right) {
            size_t mid = left + (right - left)/2;
            const void* mid_ptr = arr + mid*size;
            int res = cmp(key, mid_ptr);
            if (res < 0) right = mid;
            else if (res > 0) left = mid+1;
            else return (void*)mid_ptr;
        }
        return NULL;
    }
    
    int main() {
        double arr[] = {1.0, 2.5, 3.7, 8.9};
        double key = 3.7;
        double* found = bsearch_generic(&key, arr, 4, sizeof(double), cmp_double);
        if (found) printf("Achou: %lf\n", *found);
        else printf("Não achou\n");
        return 0;
    }
    
  4. Explique os riscos de usar void* e como mitigá-los. Dê pelo menos três exemplos de problemas que podem ocorrer.

    ✓ Resposta: Riscos: 1) Casting incorreto pode causar leitura/escrita inválida (undefined behavior). 2) Alinhamento: alguns tipos exigem alinhamento específico; void* pode não garantir. 3) Gerenciamento de memória: se o código genérico liberar memória sem saber o tipo, pode causar corrupção. Mitigações: documentar, usar callbacks, usar uniões com tags, verificar alinhamento com alignof.
  5. Crie uma função map genérica que aplica uma função a cada elemento de um array, modificando-o in-place. A função deve receber o array, tamanho, tamanho do elemento e um ponteiro para função que opera sobre void*. Teste com um array de inteiros, aplicando uma função que dobra o valor.

    ✓ Resposta:
    #include <stdio.h>
    
    typedef void (*map_func)(void*);
    
    void map(void* base, size_t count, size_t size, map_func f) {
        char* arr = base;
        for (size_t i=0; i<count; i++)
            f(arr + i*size);
    }
    
    void dobra_int(void* p) {
        int* ip = p;
        *ip *= 2;
    }
    
    int main() {
        int arr[] = {1,2,3,4};
        map(arr, 4, sizeof(int), dobra_int);
        for (int i=0; i<4; i++) printf("%d ", arr[i]);
        return 0;
    }