9

Voglio creare un puntatore di funzione a una funzione che gestirà un sottoinsieme di casi per una funzione che accetta un elenco di parametri variabili. Il caso d'uso è il casting di una funzione che prende ... in una funzione che richiede un elenco specifico di parametri, in modo da poter gestire i parametri variabili senza occuparsi di va_list e di amici.In C, è mai sicuro lanciare un puntatore a funzione variadica su un puntatore a funzione con argomenti finiti?

Nel seguente codice di esempio, sto lanciando una funzione con parametri variabili a una funzione con un elenco di parametri hardcoded (e viceversa). Funziona (o succede a lavorare), ma non so se è una coincidenza dovuta alla convenzione di chiamata in uso. (L'ho provato su due diverse piattaforme basate su 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; 
} 

È un comportamento non definito? Se sì, qualcuno può dare un esempio di una piattaforma in cui questo non funzionerà, o un modo per modificare il mio esempio in modo tale che fallirà, dato un caso d'uso più complesso?


Modifica: Per affrontare le implicazioni che questo codice avrebbe rotto con argomenti più complessi, ho esteso il mio esempio:

#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; 
} 

Funziona ancora. Qualcuno può indicare un esempio specifico di quando questo fallirebbe?

$ 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

"È questo il comportamento indefinito?" Sì. Chiamare una funzione tramite un puntatore di funzione incompatibile genera sempre un comportamento non definito. –

+0

Mi sembra (almeno con l'esempio) che i parametri push nello stack vengano eseguiti in modo coerente, indipendentemente dal fatto che venga utilizzata o meno una chiamata variadic. Quindi, anche se può essere effettivamente "indefinito" secondo le specifiche (o forse non menzionato?), Il comportamento è in realtà incoerente? – mpontillo

+3

@Mike: i parametri non-variadici vengono passati attraverso i registri della CPU quando possibile (almeno su x86). Non vengono affatto messi in pila. Questa è una delle ragioni per cui non funzionerà. – AnT

risposta

8

Casting il puntatore a un diverso tipo di puntatore di funzione è perfettamente sicuro. Ma l'unica cosa che garantisce il linguaggio è che puoi in seguito ricondurlo al tipo originale e ottenere il valore del puntatore originale.

Chiamando la funzione tramite un puntatore convertito con forza in un tipo di puntatore a funzione incompatibile conduce a un comportamento non definito. Questo vale per tutti i tipi di puntatori di funzioni incompatibili, indipendentemente dal fatto che siano variadici o meno.

Il codice che hai postato produce un comportamento non definito: non al punto del cast, ma al punto della chiamata.


Cercando di inseguire esempi "dove sarebbe fallire" è uno sforzo inutile, ma dovrebbe essere facile in ogni caso, dal momento che le convenzioni passaggio di parametri (sia di basso livello e la lingua-livello) sono molto diversi.Per esempio, il codice qui sotto normalmente non "lavoro", in pratica

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); 
} 

Stampe zero invece di 0.5 (GCC)

+0

Il codice di esempio nel la domanda sta chiaramente chiamando la funzione. Non avrei molto senso lanciare il puntatore in questo contesto. – Mastermnd

+0

Bel esempio, grazie! Il tuo esempio stampa '0.500000' per me su OS X, ma' 0.000000' su Linux (x64). – mpontillo

4

Sì, questo comportamento non è definito.

Funziona così perché i puntatori si stanno allineando per il tuo attuale compilatore, piattaforma e tipi di parametri. Prova a farlo con doppio e altri tipi e probabilmente sarai in grado di riprodurre comportamenti strani.

Anche se non lo fai, questo è un codice molto rischioso.

Suppongo che tu sia infastidito dai vararg. Prendi in considerazione la definizione di un insieme comune di parametri in un'unione o una struttura, quindi passa quello.

+1

In realtà non solo infastidito; è un caso in cui ho un'API senza una versione 'v' della stessa funzione. Ad esempio, se era qualcosa come 'printf' c'è sempre' vprintf', ma questi non hanno funzioni equivalenti che prendono un 'va_list'. Non sono proprietario dell'API, quindi non posso semplicemente definire una struttura e inoltrarla. – mpontillo

+2

Sfortunatamente, in questo caso, il percorso sicuro consiste nel creare un wrapper per ogni chiamata e analizzare le vararghe – Mastermnd