Las funciones variádicas al estilo de C permiten que una función acepte un número indeterminado de argumentos de tipos arbitrarios mediante el uso de la sintaxis de elipsis .... Este mecanismo se apoya en la directiva <cstdarg>, que proporciona el tipo va_list y las macros va_start, va_arg y va_end para navegar por la pila de argumentos.
El funcionamiento se basa en una convención de llamada: el programador es responsable de informar a la función cuántos argumentos se han pasado o qué tipos se han enviado (usualmente mediante un parámetro de control como un int count o una cadena de formato como en printf). El compilador no verifica los tipos ni el número de argumentos en tiempo de compilación; simplemente confía en que la función leerá la memoria de la pila siguiendo el orden y el tipo correctos. Por ello, si intentas leer un double cuando en la pila hay un int, o si intentas leer un argumento que no fue enviado, entrarás en el terreno del Undefined Behavior (UB).
Aunque este mecanismo es fundamental para la interoperabilidad con código C legado o para implementar wrappers de APIs de C, en el C++ moderno es una técnica que debemos evitar a toda costa. En su lugar, utilizamos parameter packs [C++11] y fold expressions [C++17], que ofrecen la misma flexibilidad pero con type safety total y una resolución de tipos realizada en tiempo de compilación, eliminando el riesgo de desincronizar la lectura de la pila.
#include <iostream>
#include <cstdarg>
#include <string_view>
// Un sistema de logging que muestra la transición de C a C++ moderno.
class Logger {
public:
// Implementación estilo C (Legacy)
// Requiere pasar el número de argumentos explícitamente para evitar UB.
void log_c_style(int count, ...) {
va_list args;
va_start(args, count); // Inicializa la lista con el último parámetro con nombre
std::cout << "[C-Style Log]: ";
for (int i = 0; i < count; ++i) {
// El programador debe saber qué tipo de dato esperar.
// Aquí asumimos que todos los argumentos son de tipo const char*
const char* msg = va_arg(args, const char*);
std::cout << msg << " ";
}
std::cout << std::endl;
va_end(args); // Limpieza obligatoria de la lista
}
// Implementación moderna (C++17)
// Utiliza variadic templates y fold expressions.
// Es segura, rápida (el compilador genera una versión para cada combinación de tipos)
// y no requiere conocer el número de argumentos de antemano.
template <typename... Args>
void log_modern(Args&&... args) {
std::cout << "[Modern Log]: ";
// Fold expression: expande la operación sobre todos los elementos del pack
(std::cout << ... << std::forward<Args>(args)) << std::endl;
}
};
int main() {
Logger logger;
// Uso correcto de la versión C: especificamos que enviamos 3 strings.
logger.log_c_style(3, "Error", "en", "sistema");
// Uso de la versión moderna: el compilador infiere los tipos de todo.
// Podemos mezclar enteros, strings, doubles, etc., sin riesgo.
logger.log_modern("Status: ", 200, " - OK", 3.1416f);
return 0;
}
En el código anterior, la función log_c_style utiliza va_start(args, count) para prepararse para leer los argumentos que siguen al parámetro count. A través de un bucle for, llamamos a va_arg(args, const char*) exactamente count veces. Es crucial entender que va_arg no “sabe” que estamos extrayendo strings; simplemente incrementa el puntero interno de la lista de argumentos según el tamaño del tipo proporcionado. Si el count fuera mayor al número de argumentos reales, leeríamos basura de la pila o intentaríamos acceder a memoria protegida.
Por el contrario, log_modern emplea un parameter pack Args.... No hay manipulación de punteros ni de la pila de forma manual. La expresión (std::cout << ... << std::forward<Args>(args)) es una fold expression de tipo unario a la izquierda. El compilador expande esto en una secuencia de llamadas a operator<< concatenadas. Si pasas un int y un std::string, el compilador simplemente busca la sobrecarga de operator<< correspondiente para cada tipo. Si hay un error de tipo, el error ocurre en tiempo de compilación, no cuando el programa ya está corriendo.
El error frecuente
El error más peligroso con va_arg es el desajuste de tipos (type mismatch) o la desincronización del conteo.
void dangerous_sum(int count, ...) {
va_list args;
va_start(args, count);
int total = 0;
for (int i = 0; i < count; ++i) {
// ERROR: El programador cree que todos son int,
// pero el llamador pasó un double.
total += va_arg(args, int);
}
va_end(args);
std::cout << total << std::endl;
}
// Llamada errónea:
// dangerous_sum(1, 3.14);
En este caso, va_arg(args, int) intentará interpretar los 8 bytes de un double como si fueran un int. El resultado es un valor totalmente erróneo y, dependiendo de la arquitectura y la convención de llamada (especialmente en sistemas con argumentos pasados por registros como x86_64), esto puede corromper el estado de la pila y causar un crash catastrófico. Este tipo de errores son invisibles para el compilador y no serán detectados por el análisis estático estándar; solo podrías capturarlos usando AddressSanitizer (-fsanitize=address) si el desajuste provoca un acceso ilegal a la memoria.
N° 34