18

関数ポインターの構造を使用して、さまざまなバックエンドのインターフェイスを実装します。シグニチャは大きく異なりますが、戻り値はほとんどすべてvoid、void *、ま​​たはintです。


struct my_interface {
    void  (*func_a)(int i);
    void *(*func_b)(const char *bla);
    ...
    int   (*func_z)(char foo);
};

ただし、バックエンドがすべてのインターフェイス機能の機能をサポートしている必要はありません。したがって、2つの可能性があります。最初のオプションは、すべての呼び出しの前に、ポインターが等しくないNULLであるかどうかを確認することです。読みやすさとパフォーマンスへの影響を恐れているので、私はそれがあまり好きではありません(ただし、私はそれを測定していません)。もう1つのオプションは、ダミー関数を使用することです。まれに、インターフェイス関数が存在しない場合があります。

したがって、署名ごとにダミー関数が必要になります。異なる戻り値に対して1つだけ持つことができるのではないかと思います。そしてそれを与えられた署名にキャストします。


#include <stdio.h>

int nothing(void) {return 0;}

typedef int (*cb_t)(int);

int main(void)
{
    cb_t func;
    int i;

    func = (cb_t) nothing;
    i = func(1);

    printf("%d\n", i);

    return 0;
}

このコードをgccでテストしたところ、機能しました。しかし、それは正気ですか?または、スタックを破損したり、他の問題を引き起こしたりする可能性がありますか?

編集:すべての答えのおかげで、私はもう少し読んだ後、規約の呼び出しについて多くを学びました。そして今、内部で何が起こっているのかをよりよく理解することができます。

4

7 に答える 7

16

C 仕様では、関数ポインターをキャストすると、未定義の動作が発生します。実際、しばらくの間、GCC 4.3 プレリリースでは、関数ポインタをキャストするたびに NULL が返されましたが、これは仕様上完全に有効でしたが、多くのプログラムが壊れたため、リリース前にその変更を撤回しました。

GCC が現在の動作を継続すると仮定すると、デフォルトの x86 呼び出し規則 (およびほとんどのアーキテクチャのほとんどの呼び出し規則) で問題なく動作しますが、私はそれに依存しません。すべてのコールサイトで関数ポインターを NULL に対してテストすることは、関数呼び出しよりもはるかに高価ではありません。本当に必要な場合は、マクロを記述できます。

#define CALL_MAYBE(func, args...) do {if (func) (func)(## args);} while (0)

または、署名ごとに異なるダミー関数を使用することもできますが、それを避けたいと考えていることは理解できます。

編集

チャールズ・ベイリーがこれについて私を呼んだので、私は行って詳細を調べました(私の穴だらけの記憶に頼るのではなく). C仕様は言う

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

そして、GCC 4.2 プレリリース (これは 4.3 より前に解決されました) は次の規則に従っていました: 関数ポインターのキャストは、私が書いたように NULL にはなりませんでしたが、互換性のない型を介して関数を呼び出そうとしました。

func = (cb_t)nothing;
func(1);

あなたの例から、結果はabort. 彼らは 4.1 の動作 (許可するが警告する) に戻しました。これは、この変更が OpenSSL を壊したことが一因ですが、OpenSSL はその間に修正されており、これはコンパイラがいつでも自由に変更できる未定義の動作です。

OpenSSL は、関数ポインターを他の関数型にキャストして、正確に同じサイズの同じ数の値を取得して返すだけでした。これは (浮動小数点を扱っていないと仮定すると)、すべてのプラットフォームと呼び出し規則でたまたま安全です。知ってる。ただし、それ以外のものは潜在的に安全ではありません。

于 2008-10-09T20:42:45.960 に答える
3

未定義の動作が発生すると思われます。

(適切なキャストを使用して) 関数へのポインターを別のシグネチャを持つ関数への別のポインターに割り当てることができますが、それを呼び出すと、奇妙なことが起こる可能性があります。

あなたのnothing()関数は引数を取りません。コンパイラにとって、これは、引数がないため、スタックの使用を最適化できることを意味する場合があります。しかし、ここで引数を指定して呼び出します。これは予期しない状況であり、クラッシュする可能性があります。

標準で適切なポイントが見つかりませんが、関数ポインターをキャストできると書かれていることを覚えていますが、結果の関数を呼び出すときは、正しいプロトタイプを使用する必要があります。そうしないと、動作が未定義になります。

補足として、関数ポインターをデータ ポインター (NULL など) と比較しないでください。3 つのポインターは別々のアドレス空間に属している可能性があるからです。C99 標準には、この特定のケースを許可する付録がありますが、広く実装されているとは思いません。とはいえ、アドレス空間が 1 つしかないアーキテクチャでは、関数ポインタをデータ ポインタにキャストするか、それを NULL と比較すると、通常は機能します。

于 2008-10-09T19:48:34.417 に答える
1

スタックが破損するリスクがあります。そうは言っても、extern "C"リンケージを使用して(および/または__cdeclコンパイラーによっては)関数を宣言すると、これを回避できる可能性があります。printf()その場合、などの関数が呼び出し元の裁量で可変数の引数を取ることができる方法に似ています。

これが現在の状況で機能するかどうかは、使用している正確なコンパイラオプションによっても異なる場合があります。MSVCを使用している場合は、デバッグとリリースのコンパイルオプションが大きな違いを生む可能性があります。

于 2008-10-09T19:39:06.680 に答える
0

大丈夫なはずです。呼び出し元は呼び出し後にスタックをクリーンアップする責任があるため、スタックに余分なものを残してはなりません。呼び出し先(この場合はnothing())は、スタック上のパラメーターを使用しようとしないため、問題ありません。

編集:これは、通常Cのデフォルトであるcdecl呼び出し規約を前提としています。

于 2008-10-09T19:37:13.143 に答える
0

これは、実装固有/プラットフォーム固有のものを使用して正しい呼び出し規約を強制しない限り機能しません。一部の呼び出し規約では、呼び出された関数がスタックのクリーンアップを担当するため、何がプッシュされたかを知る必要があります。

NULLをチェックしてから、電話をかけます。パフォーマンスに影響があるとは思えません。

コンピュータは、何よりも速くNULLをチェックできます。

于 2008-10-09T21:00:06.223 に答える
0

呼び出し先 (__cdecl) ではなく、呼び出し元がスタックのバランスをとるメソッドを使用して呼び出しを行っていることを保証できる限り。呼び出し規約を指定していない場合は、グローバル規約を別のものに設定できます。(__stdcall または __fastcall) どちらもスタックの破損につながる可能性があります。

于 2008-10-09T19:49:51.073 に答える
-1

関数ポインターを NULL にキャストすることは、C 標準では明示的にサポートされていません。あなたはコンパイラの作者に翻弄されています。多くのコンパイラで問題なく動作します。

関数ポインターに NULL または void* に相当するものがないことは、C の大きな悩みの 1 つです。

コードを防弾にしたい場合は、独自の null を宣言できますが、関数の型ごとに 1 つ必要です。例えば、

void void_int_NULL(int n) { (void)n; abort(); }

そして、あなたはテストすることができます

if (my_thing->func_a != void_int_NULL) my_thing->func_a(99);

醜いですか?

于 2008-12-06T01:20:43.227 に答える