Intro

Corrección de los ejemplos de código después de los comentarios de Avelino Herrera =)

Las funciones variádicas son funciones que aceptan un número variable de parámetros. Un ejemplo puede ser la clásica función de C: printf

#include <stdio.h>
int main(int argc, char *argv[]) {
    printf("%s%s\n", "Hola! bienvenido a ", "hardfloat =)");
    return 0;
}

Que imprime la cadena: Hola! bienvenido a hardfloat =)

Nosotros también podemos crear nuestras propias funciones variádicas, vamos a realizar nuestra implementación de printf.

Sintaxis

La sintaxis de una función variádica es bastante sencilla, especificamos los argumentos y con puntos suspensivos hacemos referencia al resto de argumentos.

void add(int num_args, ...) {
}

En el cuerpo de la función, es necesario usar una serie de macros para acceder a los argumentos que le hayamos pasado a la función. Estos son:

  • va_list: información necesaria para el resto de macros
  • va_start: acceso a los argumentos de la función variádica
  • va_arg: al siguiente argumento de la función variádica
  • va_end: el fin del acceso a los argumentos

Es mejor verlo en código. La siguiente función tiene como objetivo sumar todos los enteros que se le hayan pasado a la función.

#include <iostream>
#include <stdarg.h>
using namespace std;

int add(int num_args, ...) {
    va_list args;
    va_start(args, num_args);
    int total = 0;
    for (int i = 0; i < num_args; i++) {
        total += va_arg(args, int);
    }
    va_end(args);
    return total;
}

int main(int argc, char* argv[]) {
    int total = add(5, 2, 1, 1, 1, 1);
    cout << "total=" << total << endl;
    return 0;
}

Hay que notar que es necesario una variable, que en este caso es num_args, que indica el número de argumentos que se le han pasado.

Pasar argumentos variádicos a una función

Para pasar argumentos variádicos a una función, es necesario que dicha función acepte va_list como argumento. Es decir, veamos un contraejemplo

#include <iostream>
#include <stdarg.h>
using namespace std;

int add(int num_args, ...) {
    va_list args;
    va_start(args, num_args);
    int total = 0;
    for (int i = 0; i < num_args; i++) {
        total += va_arg(args, int);
    }
    va_end(args);
    return total;
}

int front_add(int num_args, ...) {
    va_list args;
    va_start(args, num_args);
    int total = add(num_args, args);
    va_end(args);
    return total;
}

int main(int argc, char* argv[]) {
    int total = front_add(5, 2, 1, 1, 1, 1);
    cout << "total=" << total << endl;
    return 0;
}

¿Qué nos devuelve esto? Pues basura.

Las funciones que no van a recibir directamente argumentos variádicos, deben aceptar como argumentos va_list Para ello tenemos que modificar, front_add para que acepte va_list.

Además como es algo que viene del lenguaje C, por convención las funciones que aceptan argumentos variádicos se les pone el prefijo v, por lo que refactorizando el ejemplo quedaría asi:

#include <iostream>
#include <stdarg.h>
using namespace std;

int vadd(int num_args, va_list args) {
    int total = 0;
    for (int i = 0; i < num_args; i++) {
        total += va_arg(args, int);
    }
    return total;
}

int add(int num_args, ...) {
    va_list args;
    va_start(args, num_args);
    int total = vadd(num_args, args);
    va_end(args);
    return total;
}

int main(int argc, char* argv[]) {
    int total = add(5, 2, 1, 1, 1, 1);
    cout << "total=" << total << endl;
    return 0;
}

Si se lee con detenimiento, se puede observar como el método add se encarga de utilizar las macros va_start, va_end, etc mientras que vadd tan solo va obteniendo los parámetros del stack consecutivamente.

Implementación de printf

Ahora que ya conocemos la sintaxis, vamos a hacer algo más divertido, vamos a hacer nuestra propia implementación de printf.

Antes de empezar con el código voy a explicar el algoritmo.

  1. Iterar carácter a carácter sobre la cadena fmt que es la que contiene el texto y los caracteres de control.
  2. Si no se encuentra un carácter de control sacar ese carácter por la salida estándar (cout en este caso).
  3. Se encuentra un carácter control (‘%’), comprobar si el siguiente carácter consecutivo pertenece a nuestra lista (%s, %i)
  4. Si así es, obtener el argumento del stack. El tipo de argumento lo indica el carácter de control.
  5. Convertir argumento a cadena e imprimir por salida estándar.
  6. Avanzar caracter para saltar caracteres de control.
#include <iostream>
#include <stdarg.h>
using namespace std;

void print(const char* fmt, ...) {
    va_list args;
    va_start(args, fmt);
    while (*fmt) {
        if (*fmt == '%') {
            if (*(fmt + 1) == 's') {
                string str = string(va_arg(args, char*));
                cout << str;
                fmt++;
            }
            else if (* (fmt + 1) == 'i') {
                string str = to_string(va_arg(args, int));
                cout << str;
                fmt++;
            }
        }
        else {
            std::cout << *fmt;
        }
        fmt++;
    }
    va_end(args);
}

int main (void) {
    print("Hola bienvenido a %s, estamos en %i\n", "hardfloat.es", 2021);
    return 0;
}

Conclusión

Las funciones variadicas son útiles cuando queremos procesar un número variable de argumentos y no queremos estar concatenando llamadas una tras otra. Aunque si es cierto que es un “fastidio” tener que estar creando dos capas de funciones para usar esta funcionalidad (recordar, vadd y add).

En c++ existe un concepto llamado variadic templates o plantillas variádicas que sirve para aceptar un número variable de argumentos también. Aunque utiliza el concepto de plantillas, que es otra cosa diferente a las funciones variádicas.

Links

https://en.cppreference.com/w/c/variadic