web-dev-qa-db-ja.com

関数ポインターを別の型にキャストする

コールバックとして使用する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標準を調べましたが、「互換性のある関数ポインター」の意味の定義が見つかりません。

77
Mike Weller

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

次の状況では、動作は未定義です。

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

セクション6.3.2.3、パラグラフ8は次のとおりです。

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

つまり、関数ポインターを別の関数ポインター型にキャストし、再びキャストし直して呼び出しれば、動作するようになります。

compatibleの定義はやや複雑です。セクション6.7.5.3、段落15にあります。

2つの関数型が互換性を持つためには、両方が互換性のある戻り値型を指定する127

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

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

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

関連するルールは、セクション6.7.5.1、パラグラフ2にあります。

2つのポインタータイプが互換性を持つためには、両方が同一に修飾され、両方が互換タイプへのポインターでなければなりません。

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

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

あなたが間違いなくできないこと

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

時には機能するかもしれないが、未定義の動作でもある別の悪い考え:

  • 関数ポインターと通常のポインターの間のキャスト(例:void (*)(void)から_void*_へのキャスト)。関数ポインタは、通常のポインタと必ずしも同じサイズではありません。一部のアーキテクチャでは、追加のコンテキスト情報が含まれている可能性があるためです。これはおそらくx86では問題なく動作しますが、未定義の動作であることを忘れないでください。
115
Adam Rosenfield

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

コード全体を通して、タイプ(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

上記の回答は詳細であり、おそらく正しいです-ifあなたが標準化委員会に座っています。 AdamとJohannesは、よく研究された回答に感謝します。ただし、実際には、このコードは正常に機能します。物議を醸す?はい。 GLibは、さまざまなコンパイラ/リンカー/カーネルローダー(GCC/CLang/MSVC)を使用して、多数のプラットフォーム(Linux/Solaris/Windows/OS X)でコンパイル/動作/テストします。規格はとんでもないことだと思います。

私はこれらの答えについて考えるのに時間を費やしました。これが私の結論です。

  1. コールバックライブラリを作成している場合は、これで問題ありません。注意事項-ご自身の責任で使用してください。
  2. そうでなければ、それをしないでください。

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

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

26
kevinarpe

ポイントは本当にできるかどうかではありません。簡単な解決策は

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

優れたコンパイラーは、本当に必要な場合にのみmy_callback_helperのコードを生成します。

7
MSalters

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

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

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

Cコードはポインター型をまったく気にしない命令にコンパイルされるので、言及したコードを使用するのは非常に良いことです。 do_stuffをコールバック関数で実行し、引数としてmy_struct構造体へのポインターを指定すると、問題が発生します。

うまくいかないものを示すことで、それをより明確にできることを願っています:

int my_number = 14;
do_stuff((void (*)(void*)) &my_callback_function, &my_number);
// my_callback_function will try to access int as struct my_struct
// and go nuts

または...

void another_callback_function(struct my_struct* arg, int arg2) { something }
do_stuff((void (*)(void*)) &another_callback_function, NULL);
// another_callback_function will look for non-existing second argument
// on the stack and go nuts

基本的に、データが実行時に意味を持ち続ける限り、好きなものにポインターをキャストできます。

3
che

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

C99の6.3.2.3(アイテム1)を見てください。

Voidへのポインターは、不完全またはオブジェクト型へのポインターとの間で変換される場合があります

0
xslogic

関数呼び出しがC/C++で機能する方法を考えると、スタック上の特定のアイテムをプッシュし、新しいコードの場所にジャンプして実行し、戻り時にスタックをポップします。関数ポインタが同じ戻り値型および同じ引数の数/サイズを持つ関数を記述している場合、大丈夫です。

したがって、私はあなたがそう安全にできるはずだと思います。

0
Steve Rowe