Seleção Genérica
Funções Genéricas Tradicionais
Funções genéricas são um ponto fraco do C. Quando queremos que uma função
funcione com parâmetros de qualquer tipo, é comum recebê-los com o tipo
void *
. Como passar um argumento como void *
remove as informações sobre seu
tipo original, muitas vezes a função "genérica" precisa também receber outra
função que sabe lidar com o tipo original do argumento. Aqui está a
implementação de uma função Max
genérica:
// Recebe o endereço de dois valores e uma função comparadora, e retorna o
// endereço do maior entre os dois valores utilizando o comparador.
void *Max(void *lhs, void *rhs, int cmp(void *lhs, void *rhs))
{
return cmp(lhs, rhs) < 0 ? rhs : lhs;
}
Interpretar essa declaração de Max
deve ser simples para programadores
experientes, mas para chamar a função é necessário ter um comparador definido e
isso pode ser contraproducente. É possível que por falta de conhecimento
múltiplos comparadores equivalentes sejam definidos no mesmo programa,
introduzindo redundância. Aqui está um exemplo onde há dois comparadores
equivalentes:
int CmpInt1(void *lhs, void *rhs)
{
return *(int *)lhs - *(int *)rhs;
}
int CmpInt2(void *lhs, void *rhs)
{
return *(int *)lhs - *(int *)rhs;
}
Chamar Max
com qualquer um desses comparadores terá o mesmo resultado,
portanto ter mais de um é desnecessário. Uma maneira de evitar esse problema é
criar um wrapper de Max
específico para comparar dois int
da mesma forma que
os comparadores acima.
// Comparador interno para MaxInt
static int MaxIntCmp(void *lhs, void *rhs)
{
return *(int *)lhs - *(int *)rhs;
}
int MaxInt(int lhs, int rhs)
{
return *(int *)Max(&lhs, &rhs, MaxIntCmp);
}
O problema de redundância foi resolvido. Não há mais necessidade de criar
comparadores e MaxInt
recebe e retorna valores int
diretamente, mas temos
outros problemas: O excesso de ponteiros e indireções pode impactar
consideravelmente o desempenho da função, e agora temos duas funções que fazem
essencialmente a mesma coisa: Max
e MaxInt
. Nesse ritmo, teremos também
MaxShort
, MaxUnsignedShort
, MaxUnsigned
, MaxLong
etc. e digitar um nome
diferente para realizar a mesma operação em cada tipo não agrada muitos
desenvolvedores.
Essa é uma situação comum no desenvolvimento de software: A solução de um problema causar mais problemas. Existe, afinal, alguma forma padrão de amenizar todos os problemas acima?
Palavra-chave _Generic
A palavra-chave _Generic
foi adicionada no padrão C11 e recebe uma expressão
controladora seguida de uma lista de associações. Uma associação possui a
sintaxe tipo: expressão
e o compilador escolhe qual expressão avaliar com base
no tipo da expressão controladora. Talvez você tenha morrido por dentro ao ler
isso, mas é mais simples do que parece:
int v;
// O compilador transformará isso em puts("v é um int");
_Generic(v, char: puts("v é um char"),
int: puts("v é um int"),
float: puts("v é um float"),
default: puts("v não é char, int, ou float"));
No código acima temos a expressão controladora v
e três associações. Se a
expressão controladora v
possuir o tipo char
, a seleção genérica será
substituída pela expressão puts("v é um char");
. A palavra-chave default
define uma associação para tipos que não se encaixam em nenhuma associação
anterior.
Você talvez tenha notado que a seleção genérica se comporta de forma similar à
instrução switch
, porém resolvida durante a compilação e baseada em um tipo e
não em um valor. Esse é um ponto de vista válido, mas a seleção genérica
apresenta um detalhe importante: Sua expressão controladora não é avaliada. Isso
significa que a expressão controladora puts("Olá, Mundo!")
não exibirá nada na
tela, e a expressão controladora exit(0)
não finalizará o programa. O
compilador deve saber previamente tipo de retorno dessas funções, portanto não
há necessidade de executá-las para realizar a seleção.
Enfim, como utilizar isso para resolver os problemas de nossas funções Max
e
MaxInt
?
#define MAX(lhs, ...) _Generic(lhs, int: MaxInt,\
default: Max)(lhs, __VA_ARGS__)
void *Max(void *lhs, void *rhs, int cmp(void *lhs, void *rhs))
{
return cmp(lhs, rhs) < 0 ? rhs : lhs;
}
int MaxInt(int lhs, int rhs)
{
return lhs < rhs ? rhs : lhs;
}
Agora não precisamos utilizar Max
ou MaxInt
explicitamente. Basta utilizar o
macro MAX
com os parâmetros desejados, e o compilador selecionará a função
mais apropriada:
// Comparador para floats
int CmpFloat(void *lhs, void *rhs)
{
return *(float *)lhs == *(float *)rhs ? 0 :
*(float *)lhs < *(float *)rhs ? -1 :
1;
}
int main(void)
{
int ai = 5,
bi = 3;
float af = 5.f,
bf = 3.f;
// Isso se torna int vi = MaxInt(ai, bi);
int vi = MAX(ai, bi);
// Isso se torna float vf = *(float *)Max(&af, &bf, CmpFloat);
float vf = *(float *)MAX(&af, &bf, CmpFloat);
// vi agora vale 5 e vf agora vale 5.0
return 0;
}
Como o tipo float
não possui nenhuma associação explícita na seleção genérica,
a função Max
é selecionada e precisamos utilizar ponteiros (eliminando a
possibilidade de utilizar rvalues). Para resolver isso, basta criar uma função
para comparar valores float
:
#define MAX(lhs, ...) _Generic(lhs, int: MaxInt,\
float: MaxFloat,\
default: Max)(lhs, __VA_ARGS__)
void *Max(void *lhs, void *rhs, int cmp(void *lhs, void *rhs))
{
return cmp(lhs, rhs) < 0 ? rhs : lhs;
}
int MaxInt(int lhs, int rhs)
{
return lhs < rhs ? rhs : lhs;
}
float MaxFloat(float lhs, float rhs)
{
return lhs < rhs ? rhs : lhs;
}
Agora podemos utilizar MAX
com valores float
sem ponteiros:
float vf = MAX(af, bf)
. O uso de rvalues também se torna possível:
float vf = MAX(5.f, 3.f)
.
Há vários meios de melhorar nossa seleção genérica. As funções MaxInt
e
MaxFloat
podem ser criadas com macros, pois têm a mesma estrutura. Também há
como aninhar palavras-chave _Generic
para que MAX
não precise de ponteiros
na associação default
. As possibilidades são incontáveis e não é difícil
exagerar ao ponto de tornar o código virtualmente ilegível, mas se usadas
corretamente, seleções genéricas são uma forma padronizada de lidar com diversos
problemas.