[C] Varargs e passando eles adiante

Saindo do mundo C# e voltando para o “metal” C, uma coisa que pode parecer magia para quem nunca usou são as funções variádicas – funções com um número variável de argumentos. Quem nunca se perguntou como uma função como printf é implementada? Em Python (:heart:) nós temos os velhos conhecidos *args e **kwargs, que permitem coletar argumentos extras em uma tupla (posicionais) e um dicionários (argumentos nomeados), respectivamente. Mas em C, o processo é um pouco mais complicado, envolvendo um tipo (va_list) e três macros básicas (va_start, va_arg e va_end), todos disponíveis no header <stdarg.h>.

Exemplo básico:

Abaixo segue um pequeno exemplo de uma função – sum_all – que recebe um inteiro com um contador e uma lista variável de argumentos – que esperamos serem ints –  e retorna a soma desses argumentos variáveis.

#include <stdarg.h>
#include <stdio.h>

int sum_all(int count, ...) {
    va_list args;
    va_start(args, count);
    int acc = 0;

    for (int i=0; i < count; i++) {
        int value = va_arg(args, int);
        acc += value;
    }

    va_end(args);
    return acc;
}

int main() {
    printf("%d\n", sum_all(3, 1, 5, 1000));
}

Uma função variádica é declarada usando “” no final da lista de argumentos – a qual deve conter obrigatoriamente pelo menos um argumento com nome. Dentro do corpo da função por sua vez, você declara uma variável do tipo va_list e passa essa variável para a macro va_start junto com o nome da última variável antes da lista anônima. Esse segundo argumento é importante para o código saber de onde ele deve começar a buscar os argumentos.

Uma vez que temos a va_list inicializada, podemos então passar a pegar os argumentos propriamente ditos. Para isso utilizamos a macro va_arg, que recebe como argumento a va_list inicializada e o tipo esperado do argumento. Essa segunda informação é extremamente importante para o compilador saber qual o tamanho do argumento que ele deve buscar na lista. Ao final, va_end é chamada para limpar a va_list utilizada.

CUIDADO!

Duas observações: O compilador não faz *nenhuma* checagem de tipo entre o tipo passado numa chamada da função e o tipo que va_arg tenta extrair. Nada impede por exemplo de você chamar va_arg(args, double) dentro do loop acima e receber lixo. E diferente de Python onde você recebe uma tratável exceção, em C isso pode significar o programa simplesmente dar um belo segfault na cara do usuário.

A outra observação é quanto ao problema de se descobrir quando chegamos ao final da lista de argumentos. va_arg não dá informação nenhuma a respeito, já que ele apenas extrai da pilha – onde os argumentos são guardados – um valor do tamalho do tipo fornecido. Os dois modelos mais usados para resolver isso são usar um contador/string de formato e usar sentinelas. No primeiro caso, que é como o printf e nossa sum_all acima fazem, à medida que você vai parseando a string/incrementando o contador você sabe quando deve parar ou não. Já com o uso de sentinelas, ao parsear o argumento você determina valores (ex: NULL) que ao serem lidos indicarão que chegamos ao fim da lista.

Passando adiante

Agora digamos que você tenha uma bela função variádica mas você quer “decorar” ela com outra função sua – para fins de debug/log, por exemplo. Como podemos passar esses argumentos anônimos adiante? Passar a va_list diretamente não passaria todos os argumentos diretamente tal como seria uma chamada func(*args) em Python. Na verdade, passar a va_list é equivalente a chamar func(args) – a função recebe um único argumento com a va_list.

E é aproveitando essa última informação que uma boa prática com varargs é fornecer uma variante da função alvo, só que recebendo uma va_list. Por exemplo, temos o par printf e vprintf. A primeira é variádica e na prática é implementada em termos da segunda, que recebe a va_list. Traduzindo para nosso exemplo acima:

int vsum_all(int count, va_list args) {
    int acc = 0;
    for (int i=0; i < count; i++) {
        int value = va_arg(args, int);
        acc += value;
    }
    return acc;
}

int sum_all(int count, ...) {
    va_list args;
    va_start(args, count);

    int acc = vsum_all(count, args);

    va_end(args);
    return acc;
}

E caso você necessite utilizar uma va_list com os argumentos antes de passar eles adiantes, você pode utilizar a va_copy(mylist) para incializar uma nova va_list com uma cópia da va_list original.

o/

Anúncios

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s