Outro dia no trabalho resolvemos brincar com ponteiros, não que o código que resultou da brincadeira seja algo bom, mas foi divertido. Basicamente tinhamos uma função que traduzia uma string interna passada para a string que utilizariamos em tela, software foi crescendo e mais traduções eram necessárias para a mesma string.
Então aos exemplos, do que tinhamos, da brincadeira que fizemos com os ponteiros e o que seria a melhor solução para o código. Antes algumas definições, a estrutura que contém as strings é a seguinte, basicamente um mapa 1 pra n:
enum { KERNEL = 1, WEB, CLI }; struct test { int idx; char *kernel; char *web; char *cli; }; struct test example[] = { {.idx = 0, .kernel = "eth0", .web = "Ethernet 0", .cli = "ethernet0" }, {.idx = 1, .kernel = "eth1", .web = "Ethernet 1", .cli = "ethernet1" }, {.idx = 2, .kernel = "eth2", .web = "Ethernet 2", .cli = "ethernet2" }, {.idx = 3, .kernel = NULL, .web = NULL, .cli = NULL} };
A versão original da função que procurava pela string correta era algo parecido com:
char *get_str_for_v1(const char *eth, enum str_type type) { char *rtn = NULL; int i; switch (type) { case KERNEL: for (i = 0; i < sizeof (example); i++) { if (strcmp(eth, example[i].kernel) == 0) { rtn = example[i].kernel; break; } } break; case WEB: for (i = 0; i < sizeof (example); i++) { if (strcmp(eth, example[i].kernel) == 0) { rtn = example[i].web; break; } } break; case CLI: for (i = 0; i < sizeof (example); i++) { if (strcmp(eth, example[i].kernel) == 0) { rtn = example[i].cli; break; } } break; } return rtn; } [/sourcecode] O que é obviamente muito feio! Com excesso de código repetido, péssimo de ler e dar manutenção. Vendo esse código propus o desafio de escrever a mesma função com um <code>for</code> só. Meu colega de trabalho resolveu usar ponteiros pra resolver isso, calculando o offset de cada um dos membros da estrutura e recuperando o ponteiro da string a partir do offset. Claro, isso parece muito simples de se fazer... mas na hora de escrever o código, demorou bem mais do que imaginavamos, mas foi bastante divertido entender como funcionam os casts em C. O resultado foi o seguinte código, as macros eu adicionei para facilitar a leitura do código, mais sobre elas daqui um pouco... #define member_offset(type, member) \ ((unsigned long)(&((type *)0)->member)) #define get_member_by_offset(ptr, type, offset) \ ((type *)((void *)(ptr) + offset)) char *get_str_for_pointer_fun(const char *eth, enum str_type type) { int i; unsigned long offset; switch(type) { case KERNEL: offset = member_offset(struct test, kernel); break; case WEB: offset = member_offset(struct test, web); break; case CLI: offset = member_offset(struct test, cli); break; } for (i = 0; i < sizeof (example); i++) { if (strcmp(eth, example[i].kernel) == 0) { return *get_member_by_offset(&example[i], char *, offset); } } return NULL; } [/sourcecode] Com certeza não é a melhor implementação possível, talvez seja a mais complexa de se entender, especialmente se não for utilizada uma macro para esconder a implementação. Mas é uma implementação divertida, ou pelo menos foi divertido tentar achar a combinação correta de ponteiros para gerá-la. E finalmente o código que eu julgo o mais aceitável: [sourcecode lang="c"] char *get_str_for_v2(const char *eth, enum str_type type) { int i; for (i = 0; i < sizeof (example); i++) { if (strcmp(eth, example[i].kernel) == 0) { switch (type) { case KERNEL: return example[i].kernel; case WEB: return example[i].web; case CLI: return example[i].cli; } } } return NULL; } [/sourcecode] <h2>Macros</h2> Bom, disse que ia explicar o funcionamento das duas macros utilizadas, então recapitulando elas: #define member_offset(type, member) \ ((unsigned long)(&((type *)0)->member)) #define get_member_by_offset(ptr, type, offset) \ ((type *)((void *)(ptr) + offset))
A macro member_offset
calcula o offset de um membro qualquer de um estrutura. Para fazer isso, ela dá um cast para um ponteiro do tipo da estrutura requerida ao valor 0. O que parece estranho a princípio, mas faz todo o sentido! Do ponteiro para zero até o membro requerido da estrutura existem os n bytes que representam o offset em memória do início da estrutura até o membro.
Munido do valor do offset, podemos então recuperar a estrutura que se encontra naquele offset utilizando a macro get_member_by_offset
. Obviamente é necessário sabermos qual o tipo de dado que se encontra naquele offset! Então a partir do ponteiro (endereço) da estrura que queremos acessar podemos somar o offset do membro requerido. Mas só somar o offset não resolve, é necessário dar os casts apropriados.
Como queremos um que o endereço calculado (ponteiro original + offset) seja um ponteiro, é necessário indicar isto ao compilador, colocando um * no cast do cálculo. Além disso, é necessário indicar que tipo de ponteiro estamos recuperando naquele endereço, portanto o cast vira (type *)
. No exemplo, o cast é (char **)
indicando corretamente que naquele endereço encontra-se uma string.
Esse mesmo método de recuperar um membro de uma estrutura é utilizado na implementação de listas no kernel do linux. A macro definida é um pouco diferente, uma vez que ela realiza estas duas operações de uma só vez, mas a explicação do seu funcionamento é a mesma! Segue a sua implementação:
#define list_entry(ptr, type, member) \ ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))
Basicamente o que este código faz é recuperar o ponteiro para a estrutura a partir de um membro qualquer dela. No caso das listas, o membro utilizado para recuperar a estrutura original é o struct list_head
. Com isso, é possível utilizar diversos struct list_head
numa mesma estrutura, adicionando ou não a estrutura alocada a uma das listas.