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


141

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

void (*callbackFunc)(int);

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

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

9
コールバック関数がコンパイル時にわかっている場合は、代わりにテンプレートを検討してください。
Baum mit Augen

4
コールバック関数を実装するときは、呼び出し元が必要とすることは何でもすべきです。あなたの質問が本当にコールバックインターフェースの設計に関するものである場合、ここに答えるのに十分な情報はここにはありません。コールバックの受信者に何をしてほしいですか?受信者に渡す必要がある情報は何ですか?通話の結果、受信者はどのような情報をあなたに返す必要がありますか?
ピートベッカー

回答:


171

要するに、std::functionやむを得ない理由がない限り使用してください。

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

std::function(C ++ 11以降)は主に関数を格納することです(関数を渡すために関数を格納する必要はありません)。したがって、たとえばメンバー変数にコールバックを格納する場合は、おそらくそれが最良の選択です。ただし、保存しない場合は、「最初の選択」として適切です。ただし、呼び出されたときにオーバーヘッドが(非常に小さい)発生するという欠点があります(パフォーマンスが非常に重要な状況では問題になる可能性がありますが、ほとんどの場合それはすべきではありません)。これは非常に「普遍的」です。一貫性があり、読みやすいコードを重視し、選択したすべてのことを考えたくない場合(つまり、単純に保ちたい場合)は、std::function渡すすべての関数します。

3番目のオプションについて考えます。提供されたコールバック関数を介して何かを報告する小さな関数を実装しようとしている場合は、テンプレートパラメーターを検討しますテンプレートパラメーターは呼び出し可能なオブジェクト、つまり関数ポインター、ファンクター、ラムダ、 astd::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


9
関数ポインターには、テンプレートパラメーターと比較して呼び出しオーバーヘッドがあります。実行されるコードは値ではなくパラメーターのタイプによって記述されるため、テンプレートパラメーターを使用すると、14レベル下に渡されてもインライン化が容易になります。そして、テンプレートの戻り値の型に格納されるテンプレート関数オブジェクトは、一般的で有用なパターンです(優れたコピーコンストラクターを使用するstd::functionと、すぐに外部に格納する必要がある場合に、型消去されたものに変換できる呼び出し可能な効率的なテンプレート関数を作成できます。コンテキストと呼ばれます)。
Yakk-Adam Nevraumont 2014

1
@tohecz C ++ 11が必要かどうかをここで述べます。
リーム2014

1
@ヤクああもちろん、そのことを忘れてしまった!追加、ありがとう。
leemes

1
@MooingDuckもちろん実装によって異なります。しかし、私が正しく覚えている場合、型の消去がどのように機能するかにより、もう1つの間接指定が行われますか?しかし、もう一度考えてみると、関数ポインタまたはキャプチャレスラムダを割り当てた場合(これは典型的な最適化として)、これは当てはまらないと思います
leemes

1
@leemes:右、関数ポインタやcapturelessのラムダのために、それがあるべき C-FUNC-PTRと同じオーバーヘッドを持っています。これはまだパイプラインストールです+簡単にインライン化されていません。
Mooing Duck

24

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適切に機能することを確認します。


1
これはvoid*どこから来たのですか?機能を超えて状態を維持したいのはなぜですか?関数には、必要なすべてのコード、すべての機能を含める必要があります。必要な引数を渡して、変更して何かを返します。外部状態が必要な場合、なぜfunctionPtrまたはコールバックがその荷物を運ぶのでしょうか?コールバックは不必要に複雑だと思います。
ニコス2018

@ nik-lzコメントで、Cでのコールバックの使用と履歴をどのように教えるかわかりません。または、関数型プログラミングではなく手続き型の哲学。だから、あなたは満たされないままにしておきます。
Yakk-Adam Nevraumont 2018

忘れましたthis。それは、メンバー関数が呼び出されるケースを考慮する必要があるためthisですか?それで、オブジェクトのアドレスを指すポインターが必要ですか?もし私がそれについて多くを見つけることができないので、私が間違っているなら、私がこれについてのより多くの情報を見つけることができる場所へのリンクを私に与えることができます。前もって感謝します。
Nikos 2018

@ Nik-Lzメンバー関数は関数ではありません。関数には(ランタイム)状態はありません。コールバックは、void*ランタイム状態の送信を許可するためにa を取ります。void*void*引数を持つ関数ポインターは、オブジェクトへのメンバー関数呼び出しをエミュレートできます。「Cコールバックメカニズム101の設計」をたどるリソースがわかりません。
Yakk-Adam Nevraumont 2018

ええ、それは私が話していたものです。ランタイム状態は、基本的に呼び出されるオブジェクトのアドレスです(実行間で変化するため)。それはまだですthis。そういう意味です。とにかくありがとう。
ニコス2018

17

std::function任意の呼び出し可能なオブジェクトを格納するために使用します。これにより、ユーザーはコールバックに必要なコンテキストを提供できます。単純な関数ポインタではできません。

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


ここのpのタイプは何ですか?std :: functionタイプになりますか?void f(){}; 自動p = f; p();
SREE

14

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

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


1

std::function 場合によっては、VMTをコードに組み込む可能性があり、パフォーマンスにある程度の影響があります。


3
このVMTについて説明できますか?
Gupta
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.