9

私は、可変パラメータリストを取る関数のケースのサブセットを扱う関数への関数ポインタを作成したいと思います。ユースケースでは、...という関数を特定のパラメータリストを持つ関数にキャストしているため、va_listやお友達を扱わずに可変パラメータを扱うことができます。Cでは、可変長の関数ポインタを有限の引数を持つ関数ポインタにキャストするのは安全ですか?

次のコード例では、変数パラメータを持つ関数を、ハードコードされたパラメータリストを持つ関数にキャストしています(逆もまた同様です)。これはうまくいく(あるいはうまくいく)が、私が使用している呼び出し規約のために偶然であるかどうかはわかりません。 (私は2つの異なる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; 
} 

これは未定義の動作ですか?はいの場合は、誰かがこれがうまくいかないプラットフォームの例を与えることができますか、より複雑なユースケースを考えれば、失敗するというような方法で私の例を微調整する方法はありますか?


編集:このコードは、より複雑な引数を持つ破るという意味合いに対処するために、私は私の例を拡張:

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

それはまだ動作します。誰がこれが失敗するのかという具体的な例を指摘できますか?

$ 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

は「この未定義の動作ですか?」ではないだろうはい。互換性のない関数ポインタを介して関数を呼び出すと、常に未定義の動作が発生します。 –

+0

バリデーションコールが使用されているかどうかにかかわらず、スタックにパラメータをプッシュすることは、一貫した方法で行われているようです。だから実際にはスペック(または多分言及されていない?)ごとに "未定義"かもしれませんが、その振る舞いは実際には一貫していませんか? – mpontillo

+3

@Mike:非可変パラメータは、可能であれば(少なくともx86では)CPUレジスタに渡されます。彼らはスタックにまったく押し込まれません。これがうまくいかない理由の1つです。 – AnT

答えて

8

キャスティング異なる関数ポインタ型へのポインタは完全に安全です。しかし、言語が保証する唯一のことは、後で元の型に戻して元のポインタ値を取得できることです。

ポインタを介して強制的に互換性のない関数ポインタ型に変換された関数による関数は、未定義の動作につながります。これは、可変であるかどうかにかかわらず、すべての互換性のない関数ポインタ型に適用されます。

投稿したコードでは、未定義の動作が発生します。キャスト時点ではなく、呼び出し時点です。

パラメータの受け渡し規則(低レベルと言語レベルの両方)が大幅に異なるため、無意味な努力ですが、それはとにかく簡単にする必要があり、「それは失敗する」の例を追いかけしようと

。例えば、以下のコードは、通常

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

プリントが0.5の代わりにゼロ実際には「仕事は」(GCC)

+0

問題は明らかに関数を呼び出しています。このコンテキストでポインタをキャストするだけでは意味がありません。私は恐れています。 – Mastermnd

+0

いい例、ありがとう!あなたの例では、OS Xでは '0.500000'、Linux(x64)では' 0.000000'が表示されます。 – mpontillo

4

はい、これは未定義の動作です。

ポインタは現在のコンパイラ、プラットフォーム、およびパラメータタイプのために並んでいるので、ちょうどうまくいきます。ダブルスと他のタイプでこれをやってみてください。あなたはおそらく奇妙な振る舞いを再現することができます。

そうでない場合でも、これは非常に危険なコードです。

私はあなたがvarargsに悩まされていると仮定しています。共用体または構造体に共通のパラメーター・セットを定義し、それを渡すことを検討してください。

+1

実際には煩わしいだけではありません。同じ関数の 'v'バージョンのないAPIを持っている場合があります。たとえば、 'printf'のようなものだった場合、常に' vprintf'がありますが、これらは 'va_list'を取る同等の機能を持っていません。私はそのAPIを所有していないので、構造体を定義して渡すことはできません。 – mpontillo

+2

残念なことに、この場合、安全なルートは各呼び出しのラッパーを作成し、varargsを解析することです。 – Mastermnd

関連する問題