関数ポインタで型情報の異なる関数を実行する -引数省略-

これ、知らなかった。

‚b‚ł͈ø”È—ª‚̊֐”’è‹`fnc( )‚Ífnc(void)‚Å‚È‚­fnc(...)

C言語において
  int fnc( )
という引数を省略した表現は、
  int fnc(...)
という、引数がいくつあってもよい、という意味になる ... が省略されている状態であって
  int fnc(void)
のような、引数が一つもない、という意味の宣言ではありません。

だからといって、以下のように書くとコンパイルエラーになる。

/* hikisu.c */
#include <stdio.h>

int func_dotarg(...) { return 0; }

int main(void)
{
    func_dotarg();
    return 0;
}
hikisu.c:3:17: error: ISO C requires a named argument before ‘...’


引数を省略したときには、「"..."に似た何か」になるみたい。以下で議論されてた。
C¸À¸ì´Ø·¸·Ç¼¨ÈÄ

以下のコードは未定義動作を引き起こします。
void func(){}
int main(){func(0);}


関数定義の一部である関数宣言子で識別子並びが空の場合, 関数が仮引数をもたないことを指定する。
JIS X3010:2003 6.7.5.3


呼び出される関数を表す式によって指される式の型が関数が定義された型と適合しない場合,その動作は,未定義とする。
JIS X3010:2003 6.5.2.2


以下のコードは未定義動作を引き起こします。
void func();
void func(){}
int main(){func(0);}


コンパイルは通りますが、それはエラーの検出機能を抑制したからです。
6.5.2.2 関数呼出し では、関数原型を含まない型の関数呼出しに関して、
実引数の個数と仮引数の個数が等しくない場合, その動作は未定義とする。
とありますから、コンパイルエラーを回避できても正しくないことは確かです(すくなくとも移植性がありません)。


「仮引数の個数及び型の情報がない」ってのは,
あくまで呼び出す側に対してのもので,関数定義側では
(省略形式を含めて)仮引数の個数及び型の情報は明示ないしは明確になるようです。


第一引数も可変引数の一部にしようとする私の野望は標準の範囲では無理なようです。

ちなみに,
6.11.6 関数宣言子によると,
空の括弧と伴う関数宣言子(関数原型形式の仮引数型並びではない。)の使用は,廃止予定事項とする。


となっているから,この議論は将来無駄になりますw


なんだか、危険な匂いがプンプンする…
でも試しにこれを使って、こないだの関数ポインタで型情報の異なる関数を実行する -キャスト- - ryochack.clipboardを改善できないかと試してみた。

/* pfunc.c */
#include <stdio.h>

/* それぞれ引数情報の異なる関数を複数定義 */
void func_c2(char cx, char cy) {
    printf("func_c2: cx=0x%02X, cy=0x%02X\n", cx, cy);
}

void func_i3(int ix, int iy, int iz) {
    printf("func_i3: ix=0x%04X, iy=0x%04X, iz=0x%04X\n", ix, iy, iz);
}

void func_l1(long lx) {
    printf("func_l1: lx=0x%08lX\n", lx);
}

void func_f1(float fx) {
    printf("func_f1: fx=%f\n", fx);
}

void func_d3(double dx, float fy, int iz) {
    printf("func_d3: dx=%f, fy=%f, iz=0x%04X\n", dx, fy, iz);
}

void func_i5(int ix, int iy, int iz, int iw, int iv) {
    printf("func_i5: 0x%04X, 0x%04X, 0x%04X, 0x%04X, 0x%04X\n", ix, iy, iz, iw, iv);
}


int main()
{
    int    ix = 0x1111,  iy = 0x2222,  iz = 0x3333;
    long   lx = 0x44444444,  ly = 0x55555555;
    char   cx = 0x66,  cy = 0x77;
    float  fx = 3.33;
    double dx = 7.77,  dy = 9.99;
    /* 引数を省略した関数ポインタを上記関数群で初期化 */
    void (* pfunc[])() = { func_i3, func_l1, func_c2, func_f1, func_d3, func_i5 };

    pfunc[0](ix, iy, iz);    /* func_i3 */
    pfunc[1](lx);            /* func_l1 */
    pfunc[2](cx, cy);        /* func_c2 */
    pfunc[3](fx);            /* func_f1 */
    pfunc[4](dx, fx, ix);    /* func_d3 */
    printf("--------------------\n");
    pfunc[0](cx, dy, iz);    /* func_i3:型が異なる */
    pfunc[1](lx, ly);        /* func_l1:引数が多い */
    pfunc[2](fx, dy);        /* func_c2:型が異なる */
    pfunc[3](ix, ly);        /* func_f1:引数が多い, 型が異なる */
    pfunc[4](dx);            /* func_f3:引数が少ない */
    pfunc[5]();              /* func_i5:引数が少ない */

    return 0;
}

実行結果

$ ./a.out
func_i3: ix=0x1111, iy=0x2222, iz=0x3333
func_l1: lx=0x44444444
func_c2: cx=0x66, cy=0x77
func_f1: fx=0.000000
func_d3: dx=7.770000, fy=0.000000, iz=0x1111
--------------------
func_i3: ix=0x0066, iy=0x3333, iz=0x3333
func_l1: lx=0x44444444
func_c2: cx=0xFFFFFFFF, cy=0x00
func_f1: fx=0.000000
func_d3: dx=7.770000, fy=0.000000, iz=0xFFFFFFFF
func_i5: 0xFFFFFFFF, 0x3C800000, 0x400635, 0x400991, 0x0001


おお、これだと型情報を無視した関数呼び出しができるみたいだ。
ただ、いくつか問題がある。

  • float型のデータの扱いがうまくできていない
    • 関数の引数にfloatを定義していたり、呼び出し側でfloatのデータを引数に詰めたりすると、値が0.0になってしまっている。(func_f1()とfunc_d3()にfloatを詰めた箇所参照)
  • 呼び出す関数の定義よりも引数を少なくした場合、入る値は不定値。
    • func_i5()参照。


ちなみに、コンパイル時にwarning出てた。引数の型がint, long以外の型だと関数ポインタの初期化の時に不都合があるみたい。

pfunc.c: In function ‘main’:
pfunc.c:41:5: warning: initialization from incompatible pointer type
pfunc.c:41:5: warning: initialization from incompatible pointer type
pfunc.c:41:5: warning: initialization from incompatible pointer type


便利そうだけどかなり危険なところが多いので使用しない方がいいかな。


C99仕様書読んで確認してみようと思ったけど、こんなに高いのか。無料かと思ってた。
日本語で読めるC99仕様書 JIS X 3010:2003の入手方法 | okuの日記 | スラド


2011.5.29 追記
こちらにも情報発見。
引数と戻り値の省略 - 七誌の開発日記