Programação genérica com void*
Nesta aula, exploramos a programação genérica em C usando ponteiros void*, abordando a criação de estruturas de dados genéricas, casting seguro, trade-offs e exemplos práticos. Você aprenderá a escrever código reutilizável e flexível, entendendo os riscos e cuidados necessários.
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 emqsort. - 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 substituavoid*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
qsortque 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
- cppreference - Pointer declaration
- cppreference - Type
- cppreference - memcpy
- GNU C Library - Pointers
- Embedded.com - Introduction to Generic Programming in C
- Generic Programming in C (PDF)
Exercícios
- 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; } - Escreva uma função de comparação genérica para
qsortque ordene um array de estruturasPessoa(com nome e idade) por idade. Mostre como chamarqsort.✓ 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; } - 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; } - 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 comalignof. - Crie uma função
mapgené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 sobrevoid*. 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; }