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.