miércoles, 25 de febrero de 2009

Paso de objetos como argumentos a funciones variadic en Visual Studio 2005

Advertencia: éste es un artículo técnico, escrito por y dirigido a expertos en C++. No intenten esto en casa.
Supongamos que tengo una clase CL, con sus correspondientes métodos imprescindibles (constructor, constructor de copia, operador de asignación, destructor), 32 bytes de datos y un método Print, y sean a, b y c instancias de CL. Supongamos también que tengo una función variadic que recibe una cantidad arbitraria de objetos de la clase CL y ejecuta su método Print.
En código:

void Funcion(int iN,...)
{

if(iN)
{
CL a;
va_list argumentos;

va_start(argumentos,iN);

while(iN--)
{
a = va_arg(argumentos,CL);
a.Print();
}
va_end(argumentos);
}

}

(Sé que esto es algo que no debería hacerse jamás; es una de esas cosas que C++ permite, pero cuyo resultado no está definido. Dicho eso, sigamos adelante).

El código anterior NO funciona en la implementación de C++ que se incluye en Visual Studio 2005, ya que el compilador hace trampa: siempre pasa los objetos como punteros, no importa cómo los declare uno. Vale decir, si uno declara:
foo(CL a);
lo que el compilador realmente pasa a la función es un puntero a un objeto creado al hacer la llamada, no un objeto en sí mismo. Eso tiene sentido, ya que al pasar un puntero sólo tiene que poner 4 bytes en el stack, en lugar de los N = sizeof(CL) que tendría que pasar si pusiera el objeto completo en el stack. Sin embargo, el hecho de que realmente pase un puntero en el stack hace que las funciones variadic recorran incorrectamente el stack y salten al hiperespacio.
Así, pues, si llamamos a Función así:

Funcion(3,a,b,c);

al hacer va_arg el puntero en el stack avanzará al menos 32 bytes (32 == sizeof (CL)), siendo que en el stack sólo hay 16 (tres punteros + un int), y saltará a un agujero negro. Mala cosa.

La manera de resolver el problema es, por lo tanto, recibir siempre los objetos como punteros en las funciones variadic. Nuestra función quedaría:

void Funcion(int iN,...)
{

if(iN)
{
CL *a;
va_list argumentos;

va_start(argumentos,iN);

while(iN--)
{
a = va_arg(argumentos,CL *);
a->Print();
}
va_end(argumentos);
}

}

y asunto resuelto.
Por supuesto, si uno va a hacer una cosa así, resulta más sensato pasar los punteros directamente a la función:

Funcion(3,&a,&b,&c);

aunque, como la función es variadic, el compilador no tiene manera de chequear la consistencia de los argumentos. Sin embargo, la función , tal como la definimos, tiene la curiosa propiedad de que funciona exactamente igual si es llamada con punteros o con objetos (o incluso con referencias); es decir, tanto

Funcion(3,a,&b,c);

como

Funcion(3,a,b,c);

tendrán el mismo resultado.