web-dev-qa-db-ja.com

C ++でstd :: functionまたは関数ポインターを使用する必要がありますか?

C++でコールバック関数を実装する場合、Cスタイルの関数ポインターを引き続き使用する必要があります。

void (*callbackFunc)(int);

または、std :: function:を使用する必要があります

std::function< void(int) > callbackFunc;
125
worker11811

要するに、std::functionを使用する理由がない限り。

関数ポインタには、いくつかのコンテキストをキャプチャできないという欠点があります。たとえば、いくつかのコンテキスト変数をキャプチャするコールバックとしてラムダ関数を渡すことはできません(ただし、ラムダ関数をキャプチャしない場合は機能します)。したがって、オブジェクト(this- pointer)をキャプチャする必要があるため、オブジェクトのメンバー変数(つまり、非静的)を呼び出すこともできません。(1)

std::function(C++ 11以降)は、主にstore関数に使用します(渡すことで保存する必要はありません)。したがって、たとえばメンバー変数にコールバックを保存する場合は、おそらく最良の選択です。しかし、それを保存しない場合、それは良い「最初の選択」ですが、呼び出されるときにいくらかの(非常に小さな)オーバーヘッドを導入するという欠点があります(したがって、非常にパフォーマンスがクリティカルな状況では問題になるかもしれませんが、ほとんどの場合、すべきではありません)。非常に「普遍的」です:一貫性のある読みやすいコードを気にし、選択するすべてのことを考えたくない(つまり、シンプルにしたい)場合は、渡す関数ごとにstd::functionを使用します。

3番目のオプションについて考えてみましょう:提供されたコールバック関数を介して何かを報告する小さな関数を実装する場合は、template parameterを検討してください。次に、任意の呼び出し可能オブジェクト、つまり、関数ポインタ、ファンクタ、ラムダ、std::function、...になります。ここでの欠点は、(外部)関数がテンプレートになるためです。ヘッダーに実装する必要があります。一方、(外部)関数のクライアントコードはコールバックへの呼び出しを「見る」ため、コールバックへの呼び出しをインライン化できるという利点があります。これは、使用可能な正確な型情報です。

テンプレートパラメータを使用したバージョンの例(C++ 11以前の場合は&の代わりに&&を記述します):

template <typename CallbackFunction>
void myFunction(..., CallbackFunction && callback) {
    ...
    callback(...);
    ...
}

次の表でわかるように、それらにはすべて長所と短所があります。

+-------------------+--------------+---------------+----------------+
|                   | function ptr | std::function | template param |
+===================+==============+===============+================+
| can capture       |    no(1)     |      yes      |       yes      |
| context variables |              |               |                |
+-------------------+--------------+---------------+----------------+
| no call overhead  |     yes      |       no      |       yes      |
| (see comments)    |              |               |                |
+-------------------+--------------+---------------+----------------+
| can be inlined    |      no      |       no      |       yes      |
| (see comments)    |              |               |                |
+-------------------+--------------+---------------+----------------+
| can be stored     |     yes      |      yes      |      no(2)     |
| in class member   |              |               |                |
+-------------------+--------------+---------------+----------------+
| can be implemented|     yes      |      yes      |       no       |
| outside of header |              |               |                |
+-------------------+--------------+---------------+----------------+
| supported without |     yes      |     no(3)     |       yes      |
| C++11 standard    |              |               |                |
+-------------------+--------------+---------------+----------------+
| nicely readable   |      no      |      yes      |      (yes)     |
| (my opinion)      | (ugly type)  |               |                |
+-------------------+--------------+---------------+----------------+

(1)この制限を克服するための回避策があります。たとえば、追加データを追加パラメーターとして(外部)関数に渡す場合:myFunction(..., callback, data)callback(data)を呼び出します。これはCスタイルの「引数付きコールバック」です。これはC++で(およびWIN32 APIで頻繁に使用される方法で)可能ですが、C++には優れたオプションがあるため、避ける必要があります。

(2)クラステンプレート、つまり、関数を保存するクラスはテンプレートです。しかし、それは、クライアント側で、関数の型がコールバックを格納するオブジェクトの型を決定することを意味します。これは、実際のユースケースのオプションとなることはほとんどありません。

(3)C++ 11より前の場合は、 boost::function を使用します

153
leemes

void (*callbackFunc)(int);はCスタイルのコールバック関数かもしれませんが、それはひどく使用できない、ひどいデザインのものです。

適切に設計されたCスタイルのコールバックは、void (*callbackFunc)(void*, int);のように見えます。コールバックを行うコードが関数以外の状態を維持できるようにするvoid*があります。これを行わないと、呼び出し側は強制的に状態をグローバルに保存しますが、これは失礼です。

std::function< int(int) >は、ほとんどの実装でint(*)(void*, int)呼び出しよりもわずかに高価になります。ただし、一部のコンパイラではインライン化が困難です。関数ポインター呼び出しのオーバーヘッドに匹敵するstd::functionクローン実装があります( '最速の可能なデリゲート'などを参照)。

現在、コールバックシステムのクライアントは、多くの場合、コールバックの作成および削除時にリソースを設定して破棄し、コールバックの有効期間を認識する必要があります。 void(*callback)(void*, int)はこれを提供しません。

これは、コード構造(コールバックの有効期間が限られている)または他のメカニズム(コールバックの登録解除など)を介して利用できる場合があります。

std::functionは、ライフタイム管理を制限する手段を提供します(オブジェクトの最後のコピーは、忘れられると消えます)。

一般的に、パフォーマンスの問題が顕在化しない限り、std::functionを使用します。もしそうなら、私は最初に構造の変更を探します(ピクセルごとのコールバックの代わりに、あなたが私に渡すラムダに基づいてスキャンラインプロセッサを生成するのはどうですか?関数呼び出しのオーバーヘッドを些細なレベルに減らすのに十分でしょう)。 )。その後、それが持続する場合、delegateベースの可能な最速のデリゲートに基づいて書き込み、パフォーマンスの問題がなくなるかどうかを確認します。

ほとんどの場合、レガシーAPIの関数ポインター、または異なるコンパイラーが生成したコード間で通信するためのCインターフェイスの作成にのみ関数ポインターを使用します。また、ジャンプテーブル、型消去などを実装するときに内部実装の詳細として使用しました:生成と消費の両方を行い、クライアントコードが使用するために外部に公開せず、関数ポインターが必要なすべてを行う場合。

適切なコールバックライフタイム管理インフラストラクチャがある場合、std::function<int(int)>int(void*,int)スタイルのコールバックに変換するラッパーを作成できることに注意してください。したがって、Cスタイルのコールバックライフタイム管理システムのスモークテストとして、std::functionのラッピングが適切に機能することを確認します。

std::functionを使用して、任意の呼び出し可能オブジェクトを保存します。ユーザーは、コールバックに必要なコンテキストを提供できます。単純な関数ポインタはそうではありません。

何らかの理由でプレーン関数ポインタを使用する必要がある場合(おそらくC互換のAPIが必要な場合)、void * user_context引数を追加して、少なくとも(不便ではあるが)状態にアクセスできるようにする必要があります。関数に直接渡されません。

17
Mike Seymour

std::functionを避ける唯一の理由は、C++ 11で導入されたこのテンプレートをサポートしていないレガシーコンパイラのサポートです。

C++ 11より前の言語をサポートする必要がない場合、std::functionを使用すると、呼び出し側にコールバックの実装の選択肢が広がり、「プレーン」関数ポインターよりも優れたオプションになります。コールバックを実行するコードの実装の詳細を抽出しながら、APIのユーザーにより多くの選択肢を提供します。

14
dasblinkenlight

std::functionは、場合によってはVMTをコードにもたらす可能性があり、これはパフォーマンスに影響を与えます。

0
vladon