関数ポインターを使用する利点

関数ポインタについて特に「速い」ということはありません。実行時に指定された関数を呼び出すことができます。ただし、他の関数呼び出しから得られるものとまったく同じオーバーヘッドがあります (さらに、追加のポインター間接化)。さらに、呼び出す関数は実行時に決定されるため、通常、コンパイラは関数呼び出しをインライン化することはできません。そのため、関数ポインタは、場合によっては、通常の関数呼び出しよりも大幅に遅くなることがあります。

関数ポインタはパフォーマンスとは何の関係もありません。また、パフォーマンスを向上させるために使用するべきではありません。

代わりに、それらは関数型プログラミング パラダイムに非常にわずかに同意するものであり、関数をパラメーターとして渡したり、別の関数で値を返したりすることができます。

簡単な例は、一般的なソート関数です。 2 つの要素をどのように並べ替えるかを決定するには、それらを比較する方法が必要です。これは、並べ替え関数に渡される関数ポインターである可能性があり、実際には c++ の std::sort() そのまま使えます。より小さい演算子を定義しない型のシーケンスを並べ替えるように要求する場合は、比較を実行するために呼び出すことができる関数ポインターを渡す必要があります。

そして、これは私たちを優れた代替手段にうまく導きます. C++ では、関数ポインターに限定されません。多くの場合、代わりにファンクターを使用します。つまり、演算子 () をオーバーロードするクラスです。 、関数であるかのように「呼び出す」ことができるようにします。ファンクタには、関数ポインタよりも大きな利点がいくつかあります:

  • これらは柔軟性が高く、コンストラクタ、デストラクタ、メンバー変数を備えた本格的なクラスです。それらは状態を維持でき、周囲のコードが呼び出すことができる他のメンバー関数を公開する場合があります。
  • 高速です:関数ポインタとは異なり、その型は関数のシグネチャのみをエンコードします (型 void (*)(int) の変数) 何でも int を取り、void を返す関数。ファンクターの型は、呼び出す必要がある正確な関数をエンコードします (ファンクターはクラスなので、C と呼びます。呼び出す関数は常に C::operator() )。これは、コンパイラが関数呼び出しをインライン化できることを意味します。それが一般的な std::sort を作る魔法です データ型用に特別に設計された、手作業でコーディングされた並べ替え関数と同じくらい高速です。コンパイラは、ユーザー定義関数を呼び出すオーバーヘッドをすべて排除できます。
  • より安全です:関数ポインタには型の安全性がほとんどありません。それが有効な関数を指しているという保証はありません。 NULL の可能性があります。また、ポインターに関する問題のほとんどは、関数ポインターにも当てはまります。危険でエラーが発生しやすい。

関数ポインター (C) またはファンクター (C++) またはデリゲート (C#) はすべて同じ問題を解決しますが、洗練度と柔軟性のレベルは異なります。これらを使用すると、関数をファーストクラスの値として扱い、通常どおりに渡すことができます。その他の変数。関数を別の関数に渡すと、指定された時間 (タイマーが切れたとき、ウィンドウの再描画が必要なとき、または配列内の 2 つの要素を比較する必要があるとき) に関数が呼び出されます。

私が知る限り (そして、私は長い間 Java を扱っていないので、間違っている可能性もあります)、Java には直接の同等物はありません。代わりに、インターフェイスを実装するクラスを作成し、関数を定義する必要があります (Execute() と呼びます)。 、 例えば)。そして、ユーザー提供の関数 (関数ポインター、ファンクター、またはデリゲートの形で) を呼び出す代わりに、foo.Execute() を呼び出します。 .原則として C++ 実装に似ていますが、C++ テンプレートの一般性がなく、関数ポインターとファンクターを同じように扱うことができる関数構文がありません。

そこで関数ポインタを使用します:より洗練された代替手段が利用できない場合 (つまり、C で行き詰まっている場合)、ある関数を別の関数に渡す必要がある場合。最も一般的なシナリオはコールバックです。 X が発生したときにシステムが呼び出す関数 F を定義します。そこで、F を指す関数ポインターを作成し、それを問題のシステムに渡します。

本当に、John Carmack のことは忘れて、彼のコードをコピーすれば魔法のようにコードが改善されると思い込まないでください。彼が関数ポインタを使用したのは、あなたが言及したゲームが C で書かれており、優れた代替手段が利用できないためであり、関数ポインタが存在するだけでコードの実行が高速化される魔法の要素だからではありません。


これらは、実行時までターゲット プラットフォームでサポートされている機能がわからない場合に役立ちます (CPU 機能、使用可能なメモリなど)。明白な解決策は、次のような関数を書くことです:

int MyFunc()
{
  if(SomeFunctionalityCheck())
  {
    ...
  }
  else
  {
    ...
  }
}

この関数が重要なループの奥深くで呼び出される場合は、MyFunc に関数ポインタを使用する方がよいでしょう:

int (*MyFunc)() = MyFunc_Default;

int MyFunc_SomeFunctionality()
{
  // if(SomeFunctionalityCheck())
  ..
}

int MyFunc_Default()
{
  // else
  ...
}

int MyFuncInit()
{
  if(SomeFunctionalityCheck()) MyFunc = MyFunc_SomeFunctionality;
}

もちろん、コールバック関数、メモリからのバイト コードの実行、インタープリター言語の作成など、他の用途もあります。

Windows で Intel 互換のバイトコードを実行するため。これはインタープリターに役立つ場合があります。たとえば、実行可能な配列に格納された 42 (0x2A) を返す stdcall 関数は次のとおりです。

code = static_cast<unsigned char*>(VirtualAlloc(0, 6, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE));
// mov eax, 42
code[0] = 0x8b;
code[1] = 0x2a;
code[2] = 0x00;
code[3] = 0x00;
code[4] = 0x00;
// ret
code[5] = 0xc3;
// this line executes the code in the byte array
reinterpret_cast<unsigned int (_stdcall *)()>(code)();

...

VirtualFree(code, 6, MEM_RELEASE);

);


C# でイベント ハンドラーまたはデリゲートを使用するときはいつでも、効果的に関数ポインターを使用しています。

いいえ、それらは速度に関するものではありません。関数ポインターは利便性に関するものです。

ジョナサン