Variadic functions: stdarg.h e va_list

1. Introdução às Funções Variádicas

Funções variádicas são funções que aceitam um número variável de argumentos. Em C, essa capacidade é essencial para criar interfaces flexíveis, como as funções printf() e scanf() da biblioteca padrão, que podem receber diferentes quantidades e tipos de argumentos conforme a string de formato.

A motivação principal é permitir que uma única função processe quantidades imprevisíveis de dados de entrada. No entanto, funções variádicas têm limitações importantes: não há verificação de tipos em tempo de compilação para os argumentos variáveis, e o programador é responsável por determinar como e quando parar de ler argumentos. Por isso, devem ser usadas com cautela, preferindo-se alternativas como arrays ou estruturas quando o número de elementos for conhecido em tempo de compilação.

2. A Biblioteca stdarg.h

O cabeçalho stdarg.h fornece o tipo va_list e um conjunto de macros para manipular listas de argumentos variáveis. O tipo va_list é uma estrutura opaca que armazena o estado da iteração sobre os argumentos variáveis.

Para declarar uma função variádica, utiliza-se a sintaxe com reticências (...) após pelo menos um parâmetro fixo:

#include <stdarg.h>

void minha_funcao(int parametro_fixo, ...);

O parâmetro fixo é obrigatório e serve como referência para localizar os argumentos variáveis na pilha. Sem ele, as macros não conseguiriam inicializar a lista corretamente.

3. Macros Essenciais: va_start, va_arg, va_end, va_copy

va_start

Inicializa uma variável do tipo va_list para começar a percorrer os argumentos variáveis. Recebe dois argumentos: a variável va_list e o último parâmetro fixo da função.

va_list args;
va_start(args, parametro_fixo);

va_arg

Extrai o próximo argumento da lista e avança o ponteiro interno. Requer a especificação do tipo esperado para o argumento.

int valor = va_arg(args, int);

va_end

Finaliza o uso da lista de argumentos. Deve ser chamada após o processamento de todos os argumentos para realizar a limpeza necessária.

va_end(args);

va_copy

Disponível a partir do C99, permite copiar o estado de uma va_list para outra. Útil quando é necessário percorrer a lista mais de uma vez ou salvar um ponto de verificação.

va_list copia;
va_copy(copia, original);
// processar copia...
va_end(copia);

4. Implementação Prática de Funções Variádicas

Exemplo 1: Soma de números inteiros variáveis

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

int soma(int count, ...) {
    va_list args;
    int total = 0;

    va_start(args, count);

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

    va_end(args);
    return total;
}

int main() {
    printf("Soma: %d\n", soma(3, 10, 20, 30));  // Soma: 60
    printf("Soma: %d\n", soma(5, 1, 2, 3, 4, 5));  // Soma: 15
    return 0;
}

Exemplo 2: Função para imprimir múltiplas strings

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

void imprimir_strings(int count, ...) {
    va_list args;
    va_start(args, count);

    for (int i = 0; i < count; i++) {
        char *str = va_arg(args, char*);
        printf("%s ", str);
    }

    va_end(args);
    printf("\n");
}

int main() {
    imprimir_strings(3, "Olá", "mundo", "C!");
    imprimir_strings(4, "Variadic", "functions", "são", "poderosas");
    return 0;
}

Tratamento de tipos diferentes

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

void imprimir_misto(char *formatos, ...) {
    va_list args;
    va_start(args, formatos);

    for (int i = 0; formatos[i] != '\0'; i++) {
        switch (formatos[i]) {
            case 'i':
                printf("Int: %d\n", va_arg(args, int));
                break;
            case 'd':
                printf("Double: %f\n", va_arg(args, double));
                break;
            case 's':
                printf("String: %s\n", va_arg(args, char*));
                break;
        }
    }

    va_end(args);
}

int main() {
    imprimir_misto("ids", 42, 3.14, "teste");
    return 0;
}

5. Controle de Argumentos: Como Saber Quando Parar

Existem três estratégias principais para determinar o fim da lista de argumentos:

Parâmetro contador explícito: O primeiro argumento fixo indica quantos argumentos variáveis seguem. É o método mais seguro e foi usado nos exemplos anteriores.

Valor sentinela: Um valor especial (como -1 ou NULL) marca o final da lista. Exige que os valores normais nunca coincidam com a sentinela.

int soma_sentinela(int primeiro, ...) {
    va_list args;
    int total = primeiro;
    int valor;

    va_start(args, primeiro);
    while ((valor = va_arg(args, int)) != -1) {
        total += valor;
    }
    va_end(args);

    return total;
}

String de formato: Como em printf(), a string indica quantos e quais tipos de argumentos esperar. É flexível mas propensa a erros se houver incompatibilidade.

6. Cuidados e Boas Práticas

Perigo de segurança: A ausência de verificação de tipos em tempo de compilação pode causar comportamentos indefinidos. Um erro comum é usar va_arg com um tipo diferente do argumento passado.

Promoção de tipos: Argumentos passados para funções variádicas sofrem promoções padrão: char vira int, float vira double. Portanto, ao usar va_arg, deve-se especificar int para caracteres e double para floats.

// ERRADO: char é promovido a int
char c = 'A';
va_arg(args, char);  // Comportamento indefinido!

// CORRETO:
va_arg(args, int);   // char foi promovido a int

Alternativas modernas: Macros variádicas (__VA_ARGS__) e _Generic (C11) oferecem alternativas mais seguras para certos casos. Considere usá-las quando possível.

Portabilidade: O comportamento interno de va_list é dependente de implementação. Não assuma que cópias de va_list funcionam após va_end ser chamado no original.

7. Exemplo Avançado: Função de Log Personalizada

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

void log_message(const char *nivel, const char *formato, ...) {
    va_list args;
    time_t agora;
    struct tm *info_tempo;
    char timestamp[20];

    // Obter timestamp
    time(&agora);
    info_tempo = localtime(&agora);
    strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", info_tempo);

    // Imprimir cabeçalho do log
    printf("[%s] [%s] ", timestamp, nivel);

    // Processar argumentos variáveis
    va_start(args, formato);
    vprintf(formato, args);
    va_end(args);

    printf("\n");
}

int main() {
    log_message("INFO", "Usuário %s fez login do IP %s", "joao", "192.168.1.100");
    log_message("ERRO", "Falha ao abrir arquivo: %s (código %d)", "dados.txt", 404);
    log_message("DEBUG", "Valor da variável x = %f, y = %d", 3.1415, 42);

    return 0;
}

A função log_message combina vprintf() para processar a formatação dinâmica com os argumentos variáveis. É possível estender esse conceito para escrever em arquivos usando vfprintf() ou formatar strings com vsprintf().

Referências