2008-10-21 32 views
25

tengo este pedazo de código (resumido) ...¿Hay trampas usando varargs con parámetros de referencia

AnsiString working(AnsiString format,...) 
{ 
    va_list argptr; 
    AnsiString buff; 

    va_start(argptr, format); 
    buff.vprintf(format.c_str(), argptr); 

    va_end(argptr); 
    return buff; 
} 

Y, sobre la base de que el paso por referencia se prefiere que sea posible, lo cambié esta manera.

AnsiString broken(const AnsiString &format,...) 
{ 
... the rest, totally identical ... 
} 

Mi código de llamada es la siguiente: -

AnsiString s1, s2; 
    s1 = working("Hello %s", "World"); 
    s2 = broken("Hello %s", "World"); 

Pero, S1 contiene "Hola Mundo", mientras que S2 tiene "Hola (nulo)". Creo que esto se debe a la forma en que va_start funciona, pero no estoy exactamente seguro de lo que está pasando.

Respuesta

38

Si nos fijamos en lo que va_start expande a, podrá ver lo que está pasando:

va_start(argptr, format); 

se convierte en (más o menos)

argptr = (va_list) (&format+1); 

Si el formato es un tipo de valor, que se coloca en la pila justo antes de todos los argumentos variados. Si el formato es un tipo de referencia, solo la dirección se coloca en la pila. Cuando toma la dirección de la variable de referencia, obtiene la dirección o la variable original (en este caso, una AnsiString temporal creada antes de llamar a Broken), no la dirección del argumento.

Si no desea pasar alrededor de clases completo, las opciones son o bien pasar por el puntero, o poner en un argumento ficticio:

AnsiString working_ptr(const AnsiString *format,...) 
{ 
    ASSERT(format != NULL); 
    va_list argptr; 
    AnsiString buff; 

    va_start(argptr, format); 
    buff.vprintf(format->c_str(), argptr); 

    va_end(argptr); 
    return buff; 
} 

... 

AnsiString format = "Hello %s"; 
s1 = working_ptr(&format, "World"); 

o

AnsiString working_dummy(const AnsiString &format, int dummy, ...) 
{ 
    va_list argptr; 
    AnsiString buff; 

    va_start(argptr, dummy); 
    buff.vprintf(format.c_str(), argptr); 

    va_end(argptr); 
    return buff; 
} 

... 

s1 = working_dummy("Hello %s", 0, "World"); 
+2

Una buena explicación de lo que está pasando. –

+6

Este es un buen ejemplo de la proverbial abstracción con fugas. va_start parece una especie de magia, pero en realidad es solo un poco de hackers aprobados por el compilador. – Eclipse

4

Un buen análisis de por qué no desea que esto se encuentra en N0695

2

Según C++ estándares de codificación (Sutter, Alexandrescu):

varargs nunca debe ser usado con C++:

Ellos no son de tipo seguro y tienen un comportamiento INDEFINIDO para los objetos del tipo de clase, lo que probablemente cause su problema.

+0

En este caso, no es el tema de tipo de clase, ya que no son sólo los tipos char * en la va_list. Es el hecho de que el argumento que precede a la lista es una referencia. – Eclipse

+1

Estoy de acuerdo con su respuesta, sobre la macro expansión, es un buen punto y probablemente el problema real en este caso. Sin embargo, sigue siendo una mala idea mezclarlo en su código C++, ya que no hay nada que impida que un usuario (trate de) pasar una std :: string. – lefticus

+0

Sí, no puedo esperar para la plantilla variadica en C + + 0x! – Eclipse

14

Esto es lo que la C++ estándar (18.7 - Otro soporte de tiempo de ejecución) dice sobre va_start() (énfasis mío):

Las restricciones que ISO C coloca en el segundo parámetro para la macro va_start() en el encabezado <stdarg.h> son diferentes en este estándar internacional. El parámetro parmN es el identificador del parámetro más a la derecha en la lista parámetro variable de la definición de función (el que justo antes de la ...). Si el parámetro parmN se declara con una función, matriz o referencia tipo, o con un tipo que no es compatible con el tipo que resulta cuando pasa un argumento para que no hay ningún parámetro, el comportamiento no está definido.

Como han mencionado otros, el uso de varargs en C++ es peligroso si lo usa con elementos no rectos (y posiblemente incluso de otras maneras).

Dicho esto - todavía uso de printf() todo el tiempo ...

+0

Entonces, ¿qué pasaría si el argumento varargs fuera el primero en la lista de parámetros? –

+0

@Angelorf: si no hay ningún parámetro formal a la izquierda de la lista de argumentos variables, los argumentos variables son inaccesibles. Esto generalmente no es útil, con pocas excepciones. Una de esas excepciones es si necesita una firma de función que coincida con cualquier argumento. Esto se usa comúnmente como una implementación general en la metaprogramación de plantillas (por ejemplo, cuando se combinan tipos de punteros de funciones genéricas). – IInspectable

0

Nota al margen:

El comportamiento de los tipos de clases como varargs argumentos pueden ser indefinido, pero es constante en mi experiencia. El compilador introduce sizeof (clase) de la memoria de la clase en la pila. Es decir, en pseudo-código:

alloca(sizeof(class)); 
memcpy(stack, &instance, sizeof(class); 

Para un ejemplo muy interesante de esta siendo utilizado de una manera muy creativa, un aviso de que puede pasar una instancia de CString en lugar de un LPCTSTR a una función varargs directamente, y funciona, y no hay casting involucrado. Lo dejo como ejercicio para que el lector descubra cómo hicieron ese trabajo.

0

aquí está mi solución fácil (compilado con Visual C++ 2010):

void not_broken(const string& format,...) 
{ 
    va_list argptr; 
    _asm { 
    lea eax, [format]; 
    add eax, 4; 
    mov [argptr], eax; 
    } 

    vprintf(format.c_str(), argptr); 
}