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

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

セクション 6.3.2.3、パラグラフ 8 の読み:

言い換えると、関数ポインターを別の関数ポインター型にキャストし、再度キャストして呼び出すことができます。

互換の定義 やや複雑です。これは、セクション 6.7.5.3、パラグラフ 15 にあります。

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

関連するルールは、セクション 6.7.5.1、パラグラフ 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 はありません オブジェクトを使用すると、多くの悪い結果が生じます。

動作することもあるが未定義の動作であるもう 1 つの悪い考え:

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

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

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

<オール>
  • typedef int (*CompareFunc) (const void *a, const void *b)
  • 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) でコンパイル/動作/テストします。基準なんてどうでもいいことだと思います.

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

    <オール>
  • コールバック ライブラリを作成している場合は、これで問題ないかもしれません。 emptor に注意 -- 自己責任で使用してください。
  • それ以外の場合は、しないでください。
  • この回答を書いた後でよく考えてみると、C コンパイラのコードがこれと同じトリックを使用していても驚かないでしょう。そして、(ほとんど/すべて?) 最新の C コンパイラはブートストラップされているため、これはトリックが安全であることを意味します.

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


    問題は、できるかどうかではありません。些細な解決策は

    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 のコードを生成します。その場合は、そうしてよかったと思います。