9

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 
+2

"¿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. –

+0

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

+3

@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

Respuesta

8

Casting el puntero a un tipo de puntero de función diferente es perfectamente seguro. Pero lo único que garantiza el lenguaje es que luego puede devolverlo al tipo original y obtener el valor del puntero original.

Llamar a la función a través de un puntero forzado a convertirse en un tipo de puntero de función incompatible conduce a un comportamiento indefinido. Esto se aplica a todos los tipos de punteros de función incompatibles, independientemente de si son variados o no.

El código que ha publicado produce un comportamiento indefinido: no en el punto del lanzamiento, sino en el punto de la llamada.


tratando de perseguir ejemplos "donde fallaría" es un esfuerzo inútil, sino que debe ser fácil de todos modos, ya que las convenciones de paso de parámetros (tanto de bajo nivel y el lenguaje de nivel) son muy diferentes.Por ejemplo, el código de abajo normalmente no se "trabajo" en la práctica

void foo(const char *s, float f) { printf(s, f); } 

int main() { 
    typedef void (*T)(const char *s, ...); 
    T p = (T) foo; 
    float f = 0.5; 
    p("%f\n", f); 
} 

imprime cero en lugar de 0.5 (CCG)

+0

El código de ejemplo en el la pregunta es claramente llamar a la función. Me temo que lanzar el puntero en este contexto no tendría mucho sentido. – Mastermnd

+0

Buen ejemplo, gracias! Su ejemplo imprime '0.500000' para mí en OS X, pero' 0.000000' para Linux (x64). – mpontillo

4

Sí, esto es un comportamiento indefinido.

Funciona simplemente porque los punteros se alinean para su compilador actual, plataforma y tipos de parámetros. Intenta hacer esto con dobles y otros tipos y probablemente puedas reproducir un comportamiento extraño.

Incluso si no lo hace, este es un código muy arriesgado.

Supongo que está molesto por los varargs. Considere definir un conjunto común de parámetros en una unión o estructura, y luego pase eso.

+1

En realidad no solo molesto; es un caso donde tengo una API sin una versión 'v' de la misma función. Por ejemplo, si era algo así como 'printf' siempre hay' vprintf', pero estos no tienen funciones equivalentes que toman 'va_list'. No poseo esa API, así que no puedo simplemente definir una estructura y pasar eso. – mpontillo

+2

Desafortunadamente, en ese caso, la ruta segura es crear un contenedor para cada llamada y analizar los varargs – Mastermnd

Cuestiones relacionadas