Linguagem C - Funções Variádicas [Artigo]
[ Hits: 9.238 ]
Antes de começar, vamos conversar um pouco sobre abstrações, sistemas, funções e definir o que todos esses termos tem em comum com as ditas funções variádicas. Caso não esteja interessado no porquê das coisas, pode seguramente pular esta parte. Mas não recomendo isso...
Abstração é a habilidade de reduzir um problema complexo a pequenas unidades mais fáceis de manipular e entender. Através das abstrações, podemos nos preocupar apenas com *o quê* alguma coisa faz, e deixar de lado os detalhes de *como* ou *porquê* essa coisa faz alguma coisa... Essas abstrações, quando combinadas, formam um todo(sistema) maior que a simples soma das partes.
Conclui-se que a capacidade de um sistema é a soma da capacidade total das partes com a capacidade total da *interação* entre as partes. Em outras palavras, uma parcela da capacidade do sistema vem da forma como essas abstrações *interagem* entre si.
Na programação, existem diversas abstrações. Existem abstrações para processos, memória, arquivos, dados, unidades, tipos etc. Várias. A que nos interessa aqui é a abstração de processos. Um processo é definido como a execução de um programa em um ambiente que possui estados. Um processo evolui e chega a um resultado baseado no ambiente e no programa. Um programa é definido como uma sequência de instruções a serem executadas para alcançar determinado propósito. Uma função pode ser pensada como um pequeno programa, e seus estados são suas variáveis (globais, locais e parâmetros).
Toda essa conversa sobre abstrações, sistemas, processos, funções, integração e propósitos nos leva a uma pergunta muito interessante: como as funções interagem entre si? Através da Interface e de retornos.
Interfaces... Algumas tem poucos parâmetros e são inflexíveis, outras, possuem parâmetros demais e são complicadas demais. *Para o equilíbrio da interface primar você deve.* Projetar interfaces enxutas e eficientes. Não é tarefa fácil. Para tal, precisamos pensar sobre o propósito, o processo, o ambiente e o programa. Muita coisa.
Existem tutorais, artigos, livros e padrões sobre interfaces e suas arquiteturas, mas este não é um artigo sobre isso. Entretanto, a discussão é válida, pois falaremos sobre um dos métodos de *flexibilizar* uma interface, mantendo a elegância.
Imagine que você está projetando a função printf(). As vezes, o usuário quer simplesmente imprimir um texto na tela, outras, ele quer imprimir um texto com duas variáveis inteiras, outras com sete variáveis de vários tipos.
Se você optar por construir uma função que aceita vários campos (uma estrutura de dados, ponteiros para void, e mais uma quantidade imensa de parafernálias, para permitir a você saber como e o que imprimir), você, com absoluta certeza, não encontrará muitos usuários dispostos a aprender a utilizar sua função.
Infelizmente, algumas vezes não sabemos ao certo quantos argumentos serão passados a uma função quando esta for chamada. Essas funções são chamadas de funções variádicas, e em C, são definidas de forma bem simples usando reticências como último elemento da lista de parâmetros. Portanto, uma função declarada como:
int foo(int a, int b, int c, ...);
Significa que a função retorna um inteiro e toma três parâmetros definidos, a, b e c, todos do tipo inteiro. As reticências indicam que a função tomará, após os primeiros três parâmetros, um número *indefinido* de outros parâmetros - _nenhum ou vários_.
Como isso é feito? Bom, é definido pela implementação. Depende da sua máquina, do seu sistema operacional, do seu compilador etc... Mas, de forma simplificada, pense que quando uma função é chamada em C, seus argumentos são empilhados da direita para a esquerda em uma estrutura chamada... PILHA!
Quando nomeamos os argumentos, estamos dando ao compilador uma forma de encontrar tais argumentos. Por exemplo, na função foo() acima, o compilador saberá que o argumento 'a' estará no offset 0x10 da pilha, 'b' no offset 0x18 e 'c' no offset 0x1c. E toda vez que o compilador encontrar esses identificadores dentro do escopo da função, ele os traduzirá para o endereço correto.
Se você pensar bem, para que uma função variádica seja *prática*, ela precisa ler e manipular os tais argumentos "indefinidos". Para isso, a função *precisa* saber quantos* são esses outros argumentos (paradoxal, né?!), pois se não souber, entrará em um loop infinito retirando elementos da pilha até causar uma Segmentation Fault.
Pense (de novo) na função printf(). Como ela sabe o número de argumentos que você passa? É preciso escrever aquelas esquisitices com o símbolo de porcentagem (Hmm... é mesmo!)... Já tentou passar argumentos *de menos* para printf() e especialmente para scanf()? Vai causar um erro, porque ela vai acessar/escrever coisas onde não deveria.
Portanto, mesmo as funções variádicas precisam de uma maneira de saber quantos argumentos elas irão utilizar.
Se você quiser saber mais sobre os padrões de chamada de função, existem documentos sobre isso, como a ABI x86_64, a documentação do kernel Linux, do GCC, o padrão C, o POSIX... Veja a seção de referências.