105

void (*)(void*)コールバックとして使用する関数ポインターを受け入れる関数があるとします。

void do_stuff(void (*callback_fp)(void*), void* callback_arg);

さて、次のような関数があるとします。

void my_callback_function(struct my_struct* arg);

これを安全に行うことができますか?

do_stuff((void (*)(void*)) &my_callback_function, NULL);

私はこの質問を見て、「互換性のある関数ポインター」にキャストできると言っているいくつかの C 標準を見てきましたが、「互換性のある関数ポインター」の意味の定義が見つかりません。

4

7 に答える 7

136

C標準に関する限り、関数ポインターを別のタイプの関数ポインターにキャストしてからそれを呼び出すと、未定義の動作になります。付録J.2(参考情報)を参照してください。

次の状況では、動作は定義されていません。

  • ポインタは、指定された型(6.3.2.3)と互換性のない型の関数を呼び出すために使用されます。

セクション6.3.2.3、パラグラフ8は次のように述べています。

あるタイプの関数へのポインターは、別のタイプの関数へのポインターに変換され、また元に戻される場合があります。結果は元のポインタと同じになります。変換されたポインターを使用して、指定された型と互換性のない型の関数を呼び出す場合、動作は未定義です。

つまり、関数ポインターを別の関数ポインター型にキャストし、再度キャストして呼び出し、それを呼び出すことができます。

互換性の定義はやや複雑です。これは、セクション6.7.5.3のパラグラフ15に記載されています。

2つの関数型に互換性を持たせるには、両方とも互換性のある戻り型127を指定する必要があります。

さらに、パラメータタイプリストは、両方が存在する場合、パラメータの数と省略記号ターミネータの使用において一致するものとします。対応するパラメータは互換性のあるタイプでなければなりません。一方のタイプにパラメータータイプリストがあり、もう一方のタイプが関数定義の一部ではなく、空の識別子リストを含む関数宣言子によって指定されている場合、パラメーターリストには省略記号ターミネーターがなく、各パラメーターのタイプはデフォルトの引数プロモーションの適用から生じるタイプと互換性があります。一方のタイプにパラメータータイプリストがあり、もう一方のタイプが(おそらく空の)識別子リストを含む関数定義によって指定されている場合、両方がパラメーターの数で一致する必要があります。また、各プロトタイプパラメータのタイプは、デフォルトの引数昇格を対応する識別子のタイプに適用した結果のタイプと互換性がなければなりません。(型の互換性と複合型の決定では、関数または配列型で宣言された各パラメーターは調整された型を持っていると見なされ、修飾型で宣言された各パラメーターは宣言された型の非修飾バージョンを持っていると見なされます。)

127)両方の関数タイプが「古いスタイル」の場合、パラメータータイプは比較されません。

2つのタイプが互換性があるかどうかを判断するためのルールはセクション6.2.7で説明されており、かなり長いのでここでは引用しませんが、C99標準のドラフト(PDF)で読むことができます。

ここでの関連する規則は、セクション6.7.5.1のパラグラフ2にあります。

2つのポインタ型が互換性を持つためには、両方が同じように修飾され、両方が互換性のある型へのポインタである必要があります。

したがって、aはavoid* と互換性がないstruct my_struct*ため、型の関数ポインタは型の関数ポインタとvoid (*)(void*)互換性がありません。したがって、void (*)(struct my_struct*)この関数ポインタのキャストは技術的に未定義の動作です。

ただし、実際には、関数ポインタをキャストすることで安全に回避できる場合があります。x86呼び出し規約では、引数はスタックにプッシュされ、すべてのポインターは同じサイズです(x86では4バイト、x86_64では8バイト)。関数ポインターを呼び出すことは、スタック上の引数をプッシュし、関数ポインターターゲットに間接的にジャンプすることであり、マシンコードレベルでの型の概念は明らかにありません。

絶対にできないこと:

  • 異なる呼び出し規約の関数ポインター間でキャストします。あなたはスタックを台無しにし、せいぜい、クラッシュし、最悪の場合、巨大なギャップのあるセキュリティホールで静かに成功します。Windowsプログラミングでは、関数ポインタを渡すことがよくあります。Win32は、すべてのコールバック関数がstdcall呼び出し規約(マクロ、、、CALLBACKおよびPASCALすべてWINAPIが展開される)を使用することを想定しています。標準のC呼び出し規約()を使用する関数ポインタを渡すと、cdecl問題が発生します。
  • C ++では、クラスメンバー関数ポインターと通常の関数ポインターの間でキャストします。これはしばしばC++初心者をつまずかせます。クラスメンバー関数には非表示のthisパラメーターがあり、メンバー関数を通常の関数にキャストすると、this使用するオブジェクトがなくなり、繰り返しになりますが、多くの問題が発生します。

時々機能するかもしれないが、未定義の振る舞いでもある別の悪い考え:

  • 関数ポインタと通常のポインタの間のキャスト(たとえば、avoid (*)(void)をaにキャストするvoid*)。一部のアーキテクチャでは、追加のコンテキスト情報が含まれている可能性があるため、関数ポインタは必ずしも通常のポインタと同じサイズである必要はありません。これはおそらくx86で問題なく動作しますが、未定義の動作であることを忘れないでください。
于 2009-02-18T02:54:36.093 に答える
35

最近、GLib の一部のコードに関して、まったく同じ問題について質問しました。(GLib は GNOME プロジェクトのコア ライブラリであり、C で記述されています。) 私は、slots'n'signals フレームワーク全体が GLib に依存していると聞きました。

コード全体で、タイプ (1) から (2) へのキャストのインスタンスが多数あります。

  1. typedef int (*CompareFunc) (const void *a, const void *b)
  2. typedef int (*CompareDataFunc) (const void *b, const void *b, void *user_data)

次のような呼び出しでチェーンスルーするのが一般的です。

int stuff_equal (GStuff      *a,
                 GStuff      *b,
                 CompareFunc  compare_func)
{
    return stuff_equal_with_data(a, b, (CompareDataFunc) compare_func, NULL);
}

int stuff_equal_with_data (GStuff          *a,
                           GStuff          *b,
                           CompareDataFunc  compare_func,
                           void            *user_data)
{
    int result;
    /* do some work here */
    result = compare_func (data1, data2, user_data);
    return result;
}

ここで自分の目で確かめてくださいg_array_sort()http://git.gnome.org/browse/glib/tree/glib/garray.c

上記の回答は詳細であり、標準化委員会に参加している場合はおそらく正しいでしょう。Adam と Johannes のよく研究された回答は称賛に値します。ただし、実際には、このコードが問題なく機能することがわかります。物議を醸す?はい。これを考慮してください: GLib は、さまざまなコンパイラ/リンカー/カーネル ローダー (GCC/CLang/MSVC) を備えた多数のプラットフォーム (Linux/Solaris/Windows/OS X) でコンパイル/動作/テストします。基準はひどいものだと思います。

私はこれらの答えについてしばらく考えました。これが私の結論です:

  1. コールバック ライブラリを作成している場合は、これで問題ない可能性があります。警告 emptor -- 自己責任で使用してください。
  2. そうでなければ、それをしないでください。

この回答を書いた後でよく考えてみると、C コンパイラのコードがこれと同じトリックを使用していても驚かないでしょう。そして、(ほとんど/すべて?) 最新の C コンパイラはブートストラップされているため、これはトリックが安全であることを意味します。

調査すべきより重要な質問: このトリックが機能しないプラットフォーム/コンパイラ/リンカー/ローダーを誰かが見つけられますか? そのための主要なブラウニーポイント。それを好まない組み込みプロセッサ/システムがいくつかあるに違いありません。ただし、デスクトップ コンピューティング (およびおそらくモバイル/タブレット) の場合、このトリックはおそらくまだ機能します。

于 2012-12-26T17:56:42.157 に答える
12

The point really isn't whether you can. The trivial solution is

void my_callback_function(struct my_struct* arg);
void my_callback_helper(void* pv)
{
    my_callback_function((struct my_struct*)pv);
}
do_stuff(&my_callback_helper);

A good compiler will only generate code for my_callback_helper if it's really needed, in which case you'd be glad it did.

于 2009-02-18T13:27:30.427 に答える
6

戻り型とパラメーター型に互換性がある場合は、互換性のある関数型があります-基本的に(実際にはもっと複雑です:))。互換性は「同じタイプ」と同じですが、異なるタイプを使用できるようにするために少し緩めですが、「これらのタイプはほとんど同じです」という何らかの形があります。たとえば、C89では、2つの構造体は、他の点では同一であるが名前だけが異なる場合、互換性がありました。C99はそれを変えたようです。c理論的根拠文書からの引用(強くお勧めします、ところで!):

2つの異なる変換単位の構造体、共用体、または列挙型の宣言は、これらの宣言のテキストが同じインクルードファイルからのものであっても、変換単位自体が互いに素であるため、正式に同じ型を宣言しません。したがって、標準では、そのようなタイプの追加の互換性ルールが指定されているため、2つのそのような宣言が十分に類似している場合、それらは互換性があります。

そうは言っても、厳密にはこれは未定義の動作です。do_stuff関数または他の誰かがvoid*、パラメーターとして関数ポインターを使用して関数を呼び出しますが、関数に互換性のないパラメーターがあるためです。しかし、それにもかかわらず、私はすべてのコンパイラがうめき声なしでそれをコンパイルして実行することを期待しています。しかし、実際の関数を呼び出すだけの別の関数を取得するvoid*(そしてそれをコールバック関数として登録する)ことで、よりクリーンに行うことができます。

于 2009-02-18T03:02:31.210 に答える
0

void ポインターは、他のタイプのポインターと互換性があります。これは、malloc と mem 関数 ( memcpymemcmp) がどのように機能するかのバックボーンです。通常、C (C++ ではなく) ではNULL、 として定義されたマクロ((void *)0)です。

C99 の 6.3.2.3 (項目 1) を見てください。

void へのポインターは、任意の不完全型またはオブジェクト型へのポインターとの間で変換できます。

于 2011-01-19T14:36:16.093 に答える