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


88

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


1
私は少し初心者ですが、「void()(void)関数ポインタ」とはどういう意味ですか?void *を引数として受け入れ、voidを返す関数へのポインタですか
Digital Gal

2
@Myke:void (*func)(void *)は、funcなどの型シグネチャを持つ関数へのポインタであることを意味しますvoid foo(void *arg)。そうです、その通りです。
mk12 2012

回答:


121

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

ためしたがって、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使用するオブジェクトがなくなり、繰り返しになりますが、多くの問題が発生します。

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

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

18
全体的なポイントvoid*は、それらが他のポインターと互換性があるということではありませんか?にキャストするのstruct my_struct*に問題void*はないはずです。実際、キャストする必要すらなく、コンパイラはそれを受け入れる必要があります。たとえば、を受け取るstruct my_struct*関数にを渡す場合、void*キャストは必要ありません。これらを互換性のないものにするために、ここで何が欠けていますか?
brianmearns 2012年

2
この回答は、「これはおそらくx86で問題なく動作します...」に言及しています。これが動作しないプラットフォームはありますか?これが失敗したときの経験はありますか?Cのqsort()は、可能であれば関数ポインターをキャストするのに適した場所のようです。
kevinarpe 2012

4
@KCArpe:この記事の「メンバー関数ポインターの実装」という見出しの下のチャートによると、16ビットOpenWatcomコンパイラーは、特定の構成でデータポインター型(2バイト)よりも大きな関数ポインター型(4バイト)を使用することがあります。ただし、POSIX準拠のシステムではvoid*、関数ポインタ型と同じ表現を使用する必要があります。仕様を参照してください。
アダムローゼンフィールド2012

3
@adamからのリンクは、関連するセクション2.12.3が削除されたPOSIX標準の2016年版を参照するようになりました。あなたはまだ2008年版でそれを見つけることができます。
Martin Trenkmann 2016

6
@brianmearnsいいえ、非常に正確に定義された方法でvoid *他の(非関数)ポインターと「互換性がある」だけです(この場合、C標準が「互換性がある」という言葉で意味することとは無関係です)。Cを使用すると、avoid *を、より大きくしたり小さくしstruct my_struct *たり、ビットの順序を変えたり、否定したりすることができます。したがってvoid f(void *)ABIと互換性がないvoid f(struct my_struct *)可能性があります。Cは、必要に応じてポインター自体を変換しますが、指定された関数を変換して、場合によっては異なる引数タイプを取得することはできません。
mtraceur

32

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

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

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

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

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

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

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


10
それが確実に機能しない場所は、EmscriptenLLVMからJavascriptへのコンパイラです。詳細については、github.com / kripken / emscripten / wiki / Asm-pointer-castsを参照してください。
ベンリンス2013年

2
Emscriptenに関する最新のリファレンス。
ysdx 2015

4
@BenLingsが投稿したリンクは近い将来壊れます。正式にkripken.github.io/emscripten-site/docs/porting/guidelines/に
Alex

9

重要なのは、できるかどうかではありません。簡単な解決策は

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のコードを生成します。その場合は、それができてうれしいです。


問題は、これが一般的な解決策ではないということです。機能の知識があり、ケースバイケースで行う必要があります。すでに間違ったタイプの関数がある場合は、行き詰まります。
BeeOnRope 2017年

これをテストしたすべてのコンパイラmy_callback_helperは、常にインライン化されていない限り、のコードを生成します。それがする傾向がある唯一のことはであるため、これは絶対に必要ではありませんjmp my_callback_function。コンパイラはおそらく関数のアドレスが異なることを確認したいと思うでしょうが、残念ながら、関数がC99でマークされている場合inline(つまり、「アドレスを気にしない」)でもこれを行います。
yyny

これが正しいかどうかはわかりません。上記の別の返信からの別のコメント(@mtraceurによる)は、avoid *がaとは異なるサイズである可能性がある ことを示していstruct *ます(そうでmallocなければ壊れてしまうので、それは間違っていると思いますが、そのコメントには5つの賛成票があるので、私はそれにクレジットを与えています。 @mtraceurが正しければ、あなたが書いた解決策は正しいことではないでしょう。
cesss

@cesss:サイズが異なっていてもまったく問題ありません。との間の変換はvoid*まだ機能する必要があります。要するに、void*より多くのビットがあるかもしれませんが、それらにキャストするstruct*と、void*それらの余分なビットはゼロになる可能性があり、キャストバックはそれらのゼロを再び破棄することができます。
MSalters

@MSalters:私は本当にvoid *(理論的には)とはそれほど違う可能性があることを知りませんでしたstruct *。私はCでvtableを実装してthisおり、仮想関数への最初の引数としてC ++風のポインターを使用しています。明らかthisに、「現在の」(派生)構造体へのポインタである必要があります。したがって、仮想関数には、実装されている構造体に応じて異なるプロトタイプが必要です。void *this引数を使用するとすべてが修正されると思いましたが、今では未定義の動作であることが
わかりました

6

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

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

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


4

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

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


0

関数呼び出しがC / C ++でどのように機能するかを考えると、関数呼び出しはスタック上の特定の項目をプッシュし、新しいコードの場所にジャンプして実行し、戻ったときにスタックをポップします。関数ポインタが同じ戻り値の型と同じ数/サイズの引数を持つ関数を記述している場合は、問題ないはずです。

したがって、安全に行うことができるはずだと思います。


2
struct-pointersとvoid-pointersに互換性のあるビット表現がある場合にのみ安全です。それが事実であるとは限りません
クリストフ

1
コンパイラは、レジスタで引数を渡すこともできます。また、float、int、またはポインタに異なるレジスタを使用することは前例のないことではありません。
MSalters 2009

0

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

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

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


これは、Adam Rosenfieldの回答と矛盾します。最後の段落とコメントを参照してください
ユーザー

1
この答えは明らかに間違っています。関数ポインタを除いて、どのポインタもvoidポインタに変換できます。
marton78 2016
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.