Quiero crear un puntero a una función que manejará un subconjunto de casos para una función que toma una lista de parámetros variables. El caso de uso está emitiendo una función que lleva ...
a una función que toma una lista específica de parámetros, por lo que puede tratar con parámetros variables sin tener que lidiar con va_list
y sus amigos.En C, ¿es seguro lanzar un puntero de función variable a un puntero de función con argumentos finitos?
En el siguiente código de ejemplo, estoy emitiendo una función con parámetros variables para una función con una lista de parámetros codificados (y viceversa). Esto funciona (o funciona), pero no sé si es una coincidencia debido a la convención de llamadas en uso. (Lo probé en dos plataformas basadas en diferentes x86_64.)
#include <stdio.h>
#include <stdarg.h>
void function1(char* s, ...)
{
va_list ap;
int tmp;
va_start(ap, s);
tmp = va_arg(ap, int);
printf(s, tmp);
va_end(ap);
}
void function2(char* s, int d)
{
printf(s, d);
}
typedef void (*functionX_t)(char*, int);
typedef void (*functionY_t)(char*, ...);
int main(int argc, char* argv[])
{
int x = 42;
/* swap! */
functionX_t functionX = (functionX_t) function1;
functionY_t functionY = (functionY_t) function2;
function1("%d\n", x);
function2("%d\n", x);
functionX("%d\n", x);
functionY("%d\n", x);
return 0;
}
¿Este comportamiento indefinido? En caso afirmativo, ¿alguien puede dar un ejemplo de una plataforma donde esto no funcionará, o una forma de modificar mi ejemplo de tal manera que fallará, dado un caso de uso más complejo?
Editar: Para hacer frente a la implicación de que este código se rompería con argumentos más complejos, me extendió mi ejemplo:
#include <stdio.h>
#include <stdarg.h>
struct crazy
{
float f;
double lf;
int d;
unsigned int ua[2];
char* s;
};
void function1(char* s, ...)
{
va_list ap;
struct crazy c;
va_start(ap, s);
c = va_arg(ap, struct crazy);
printf(s, c.s, c.f, c.lf, c.d, c.ua[0], c.ua[1]);
va_end(ap);
}
void function2(char* s, struct crazy c)
{
printf(s, c.s, c.f, c.lf, c.d, c.ua[0], c.ua[1]);
}
typedef void (*functionX_t)(char*, struct crazy);
typedef void (*functionY_t)(char*, ...);
int main(int argc, char* argv[])
{
struct crazy c =
{
.f = 3.14,
.lf = 3.1415,
.d = -42,
.ua = { 0, 42 },
.s = "this is crazy"
};
/* swap! */
functionX_t functionX = (functionX_t) function1;
functionY_t functionY = (functionY_t) function2;
function1("%s %f %lf %d %u %u\n", c);
function2("%s %f %lf %d %u %u\n", c);
functionX("%s %f %lf %d %u %u\n", c);
functionY("%s %f %lf %d %u %u\n", c);
return 0;
}
Todavía funciona. ¿Alguien puede señalar un ejemplo específico de cuándo fallaría esto?
$ gcc -Wall -g -o varargs -O9 varargs.c
$ ./varargs
this is crazy 3.140000 3.141500 -42 0 42
this is crazy 3.140000 3.141500 -42 0 42
this is crazy 3.140000 3.141500 -42 0 42
this is crazy 3.140000 3.141500 -42 0 42
"¿Es éste un comportamiento indefinido?" Sí. Llamar a una función a través de un puntero de función incompatible siempre produce un comportamiento indefinido. –
Me parece (al menos por ejemplo) que presionar parámetros en la pila se realiza de forma consistente, independientemente de si se usa o no una llamada variadica. Entonces, si bien puede ser "indefinido" según la especificación (¿o tal vez simplemente no se menciona?), ¿El comportamiento es realmente inconsistente? – mpontillo
@Mike: los parámetros no variados se pasan a través de los registros de la CPU siempre que sea posible (al menos en x86). No están empujados en la pila en absoluto. Esta es una de las razones por las que no funcionará. – AnT