9

Ich möchte einen Funktionszeiger auf eine Funktion erstellen, die eine Teilmenge von Fällen für eine Funktion behandelt, die eine Variablenparameterliste verwendet. Der Anwendungsfall ist das Umsetzen einer Funktion, die ... zu einer Funktion führt, die eine bestimmte Liste von Parametern verwendet, so dass Sie mit variablen Parametern umgehen können, ohne sich mit va_list und Freunden zu befassen.In C, ist es jemals sicher, einen Variadic-Funktionszeiger auf einen Funktionszeiger mit endlichen Argumenten zu werfen?

Im folgenden Beispiel-Code, bin ich eine Funktion mit einer Variablen Parameter auf eine Funktion mit einer fest codierten Parameterliste (und umgekehrt). Das funktioniert (oder funktioniert zufällig), aber ich weiß nicht, ob es ein Zufall ist, aufgrund der verwendeten Aufrufkonvention. (Ich habe es auf zwei verschiedenen x86_64-basierten Plattformen probiert.)

Ist dieses undefinierte Verhalten? Wenn ja, kann jemand ein Beispiel für eine Plattform geben, wo dies nicht funktioniert, oder eine Möglichkeit, mein Beispiel so zu optimieren, dass es bei einem komplexeren Anwendungsfall fehlschlägt?


bearbeiten: die Implikation zu begegnen, dass dieser Code mit komplexeren Argumente brechen würde, erweiterte ich mein Beispiel:

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

Es funktioniert immer noch. Kann jemand auf ein konkretes Beispiel hinweisen, wann dies scheitern würde?

$ 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

„Ist das nicht definiertes Verhalten?“ Ja. Der Aufruf einer Funktion über einen inkompatiblen Funktionszeiger führt immer zu undefiniertem Verhalten. –

+0

Es scheint mir (zumindest durch ein Beispiel), dass das Schieben von Parametern auf den Stapel konsistent erfolgt, unabhängig davon, ob ein variadischer Aufruf verwendet wird oder nicht. Obwohl es in der Tat "undefiniert" sein kann (oder vielleicht nur nicht erwähnt wird), ist das Verhalten überhaupt inkonsistent? – mpontillo

+3

@Mike: Nicht-variadische Parameter werden wann immer möglich durch die CPU-Register geleitet (zumindest auf x86). Sie werden überhaupt nicht auf den Stapel geschoben. Dies ist ein Grund, warum es nicht funktioniert. – AnT

Antwort

8

Casting der Zeiger auf einen anderen Funktionszeigertyp ist vollkommen sicher. Das einzige, was die Sprache garantiert, ist, dass Sie sie später auf den ursprünglichen Typ zurückwerfen und den ursprünglichen Zeigerwert erhalten können.

Aufruf die Funktion durch einen Zeiger in den inkompatiblen Funktionszeiger umgewandelt Typ führt zu undefiniertem Verhalten. Dies gilt für alle inkompatiblen Funktionszeiger, unabhängig davon, ob sie variadisch sind oder nicht.

Der von Ihnen gepostete Code erzeugt ein undefiniertes Verhalten: nicht am Ort der Besetzung, sondern zum Zeitpunkt des Aufrufs.


Der Versuch, Beispiele zu jagen „wo es scheitern würde“ ist ein sinnloses Unterfangen, aber es sollte trotzdem leicht sein, da die Parameterübergabe Konventionen (Low-Level-und Sprachebene) in beträchtlichem Ausmaß unterschiedlich sind.Zum Beispiel unter dem Code normalerweise nicht „Arbeit“ in der Praxis

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

Drucke Null statt 0.5 (GCC)

+0

Der Beispielcode in der Frage nennt eindeutig die Funktion. Den Zeiger in diesem Zusammenhang einfach zu zeigen, würde nicht viel Sinn machen, fürchte ich. – Mastermnd

+0

Schönes Beispiel, danke! Ihr Beispiel druckt '0,500000' für mich unter OS X, aber' 0,000000' unter Linux (x64). – mpontillo

4

Ja, das ist undefiniertes Verhalten.

Es passiert einfach so, weil die Zeiger für Ihren aktuellen Compiler, Plattform und Parametertypen anstehen. Versuchen Sie das mit Doubles und anderen Typen und Sie werden wahrscheinlich seltsames Verhalten reproduzieren können.

Auch wenn Sie dies nicht tun, ist dies ein sehr riskanter Code.

Ich nehme an, Sie ärgern sich über die varargs. Erwägen Sie, einen allgemeinen Satz von Parametern in einer Union oder Struktur zu definieren, und übergeben Sie diesen dann.

+1

Eigentlich nicht nur genervt; Es ist ein Fall, wo ich eine API ohne eine 'v'-Version der gleichen Funktion habe. Zum Beispiel, wenn es etwas wie 'printf' war, gibt es immer' vprintf', aber diese haben keine äquivalenten Funktionen, die eine 'va_list' annehmen. Ich besitze diese API nicht, also kann ich nicht einfach eine Struktur definieren und diese übergeben. – mpontillo

+2

In diesem Fall besteht die sichere Route darin, für jeden Aufruf einen Wrapper zu erstellen und die Variablen zu analysieren. – Mastermnd

Verwandte Themen