C ++のコールバック関数


303

C ++では、いつ、どのようにコールバック関数を使用しますか?

編集:
私はコールバック関数を書くための簡単な例を見たいのですが。


[ this ](thispointer.com/…)は、コールバック関数の基本を非常によく説明し、概念を理解しやすくしています。
Anurag Singh

回答:


449

注:ほとんどの回答は、C ++で「コールバック」ロジックを実現するための1つの可能性である関数ポインターをカバーしていますが、今日のところ、私は最も好ましいものではないと思います。

コールバックとは(?)そしてなぜそれらを使うのか(!)

コールバックは、クラスまたは関数によって受け入れられる呼び出し可能(詳細は下を参照)であり、そのコールバックに応じて現在のロジックをカスタマイズするために使用されます。

コールバックを使用する理由の1つは、呼び出された関数のロジックに依存せず、さまざまなコールバックで再利用できる汎用コードを記述することです。

標準アルゴリズムライブラリの多くの関数は<algorithm>コールバックを使用します。たとえば、for_eachアルゴリズムは、一連のイテレータのすべてのアイテムに単項コールバックを適用します。

template<class InputIt, class UnaryFunction>
UnaryFunction for_each(InputIt first, InputIt last, UnaryFunction f)
{
  for (; first != last; ++first) {
    f(*first);
  }
  return f;
}

これは、最初にインクリメントし、次に適切な呼び出し可能オブジェクトを渡すことでベクトルを出力するために使用できます。次に例を示します。

std::vector<double> v{ 1.0, 2.2, 4.0, 5.5, 7.2 };
double r = 4.0;
std::for_each(v.begin(), v.end(), [&](double & v) { v += r; });
std::for_each(v.begin(), v.end(), [](double v) { std::cout << v << " "; });

印刷する

5 6.2 8 9.5 11.2

コールバックの別のアプリケーションは、特定のイベントの呼び出し元への通知であり、これにより、一定の静的/コンパイル時間の柔軟性が可能になります。

個人的には、2つの異なるコールバックを使用するローカル最適化ライブラリを使用しています。

  • 最初のコールバックは、関数値と入力値のベクトルに基づく勾配が必要な場合に呼び出されます(ロジックコールバック:関数値の決定/​​勾配の導出)。
  • 2番目のコールバックは、アルゴリズムステップごとに1回呼び出され、アルゴリズムの収束に関する特定の情報を受け取ります(通知コールバック)。

したがって、ライブラリデザイナーは、通知コールバックを介してプログラマーに提供される情報で何が起こるかを決定する責任を負いません。関数の値は、ロジックコールバックによって提供されるため、実際にどのように決定するかについて心配する必要はありません。これらのことを正しく行うことは、ライブラリユーザーにとっての仕事であり、ライブラリをスリムでより一般的なものに保ちます。

さらに、コールバックは動的なランタイム動作を有効にすることができます。

ユーザーがキーボードのボタンを押すたびに実行される関数と、ゲームの動作を制御する一連の関数がある、ある種のゲームエンジンクラスを想像してみてください。コールバックを使用すると、実行時に実行するアクションを(再)決定できます。

void player_jump();
void player_crouch();

class game_core
{
    std::array<void(*)(), total_num_keys> actions;
    // 
    void key_pressed(unsigned key_id)
    {
        if(actions[key_id]) actions[key_id]();
    }
    // update keybind from menu
    void update_keybind(unsigned key_id, void(*new_action)())
    {
        actions[key_id] = new_action;
    }
};

ここでは、関数key_pressedはに格納されactionsているコールバックを使用して、特定のキーが押されたときに目的の動作を取得します。プレーヤーがジャンプ用のボタンを変更することを選択した場合、エンジンは

game_core_instance.update_keybind(newly_selected_key, &player_jump);

したがって、次回このボタンがゲーム内で押されると、呼び出しの動作をkey_pressed(呼び出しplayer_jump)に変更します。

C ++(11)の呼び出し可能オブジェクトとは何ですか?

より正式な説明については、C ++の概念: cppreferenceで呼び出し可能を参照してください。

コールバック機能は、C ++(11)ではいくつかの方法で実現できます。これは、いくつかの異なることが呼び出し可能であることが判明しているためです*

  • 関数ポインター(メンバー関数へのポインターを含む)
  • std::function オブジェクト
  • ラムダ式
  • バインド式
  • 関数オブジェクト(オーバーロードされた関数呼び出し演算子を持つクラスoperator()

* 注:データメンバーへのポインターも呼び出すことができますが、関数はまったく呼び出されません。

コールバックを詳細に記述するいくつかの重要な方法

  • X.1この投稿でのコールバックの「書き込み」とは、コールバックタイプを宣言して名前を付ける構文を意味します。
  • X.2コールバックの「呼び出し」とは、それらのオブジェクトを呼び出すための構文を指します。
  • X.3コールバックの「使用」とは、コールバックを使用して関数に引数を渡すときの構文を意味します。

注:C ++ 17以降では、メンバーケースへのポインターも処理するような呼び出しをf(...)記述できますstd::invoke(f, ...)

1.関数ポインター

関数ポインターは、コールバックが持つことができる「最も単純な」(一般性の点で、読みやすさの点では間違いなく最悪)タイプです。

簡単な関数を見てみましょうfoo

int foo (int x) { return 2+x; }

1.1関数ポインタ/型表記の記述

関数ポインタ型は、表記を有します

return_type (*)(parameter_type_1, parameter_type_2, parameter_type_3)
// i.e. a pointer to foo has the type:
int (*)(int)

どこという名前の関数ポインタ型は次のようになります

return_type (* name) (parameter_type_1, parameter_type_2, parameter_type_3)

// i.e. f_int_t is a type: function pointer taking one int argument, returning int
typedef int (*f_int_t) (int); 

// foo_p is a pointer to function taking int returning int
// initialized by pointer to function foo taking int returning int
int (* foo_p)(int) = &foo; 
// can alternatively be written as 
f_int_t foo_p = &foo;

using以来宣言は、私たちに物事が少し読みやすくするためのオプションを与えるtypedefためにf_int_t:缶は次のように書くことも

using f_int_t = int(*)(int);

(少なくとも私にとっては)f_int_t新しい型エイリアスであることが明確であり、関数ポインター型の認識もより簡単です

また、関数ポインタ型のコールバックを使用する関数の宣言は次のようになります。

// foobar having a callback argument named moo of type 
// pointer to function returning int taking int as its argument
int foobar (int x, int (*moo)(int));
// if f_int is the function pointer typedef from above we can also write foobar as:
int foobar (int x, f_int_t moo);

1.2コールバック呼び出し表記

呼び出し表記は、単純な関数呼び出し構文に従います。

int foobar (int x, int (*moo)(int))
{
    return x + moo(x); // function pointer moo called using argument x
}
// analog
int foobar (int x, f_int_t moo)
{
    return x + moo(x); // function pointer moo called using argument x
}

1.3コールバック使用表記と互換タイプ

関数ポインタを使用するコールバック関数は、関数ポインタを使用して呼び出すことができます。

関数ポインタコールバックを受け取る関数を使用するのはかなり簡単です。

 int a = 5;
 int b = foobar(a, foo); // call foobar with pointer to foo as callback
 // can also be
 int b = foobar(a, &foo); // call foobar with pointer to foo as callback

1.4例

コールバックの動作に依存しない関数を記述できます。

void tranform_every_int(int * v, unsigned n, int (*fp)(int))
{
  for (unsigned i = 0; i < n; ++i)
  {
    v[i] = fp(v[i]);
  }
}

可能なコールバックは

int double_int(int x) { return 2*x; }
int square_int(int x) { return x*x; }

のように使用

int a[5] = {1, 2, 3, 4, 5};
tranform_every_int(&a[0], 5, double_int);
// now a == {2, 4, 6, 8, 10};
tranform_every_int(&a[0], 5, square_int);
// now a == {4, 16, 36, 64, 100};

2.メンバー関数へのポインター

(一部のクラスのC)メンバー関数へのポインターは、C操作する型のオブジェクトを必要とする特別なタイプの(そしてさらに複雑な)関数ポインターです。

struct C
{
    int y;
    int foo(int x) const { return x+y; }
};

2.1メンバー関数/型表記へのポインターの書き込み

一部のクラスのメンバー関数型へポインターにTは、表記法があります

// can have more or less parameters
return_type (T::*)(parameter_type_1, parameter_type_2, parameter_type_3)
// i.e. a pointer to C::foo has the type
int (C::*) (int)

メンバー関数への名前付きポインターは、関数ポインターと同様に、次のようになります。

return_type (T::* name) (parameter_type_1, parameter_type_2, parameter_type_3)

// i.e. a type `f_C_int` representing a pointer to member function of `C`
// taking int returning int is:
typedef int (C::* f_C_int_t) (int x); 

// The type of C_foo_p is a pointer to member function of C taking int returning int
// Its value is initialized by a pointer to foo of C
int (C::* C_foo_p)(int) = &C::foo;
// which can also be written using the typedef:
f_C_int_t C_foo_p = &C::foo;

例:メンバー関数コールバックへポインターを引数の1つとして取る関数の宣言:

// C_foobar having an argument named moo of type pointer to member function of C
// where the callback returns int taking int as its argument
// also needs an object of type c
int C_foobar (int x, C const &c, int (C::*moo)(int));
// can equivalently declared using the typedef above:
int C_foobar (int x, C const &c, f_C_int_t moo);

2.2コールバック呼び出し表記

のメンバー関数へのポインターは、逆参照されたポインターでメンバーアクセス操作を使用Cすることにより、タイプのオブジェクトに関して呼び出すことがCできます。 注:括弧が必要です!

int C_foobar (int x, C const &c, int (C::*moo)(int))
{
    return x + (c.*moo)(x); // function pointer moo called for object c using argument x
}
// analog
int C_foobar (int x, C const &c, f_C_int_t moo)
{
    return x + (c.*moo)(x); // function pointer moo called for object c using argument x
}

注:へのポインターCが使用可能な場合、構文は同等です(ポインターへのポインターCも逆参照する必要があります)。

int C_foobar_2 (int x, C const * c, int (C::*meow)(int))
{
    if (!c) return x;
    // function pointer meow called for object *c using argument x
    return x + ((*c).*meow)(x); 
}
// or equivalent:
int C_foobar_2 (int x, C const * c, int (C::*meow)(int))
{
    if (!c) return x;
    // function pointer meow called for object *c using argument x
    return x + (c->*meow)(x); 
}

2.3コールバック使用表記と互換タイプ

classのメンバー関数ポインターをT使用するコールバック関数は、classのメンバー関数ポインターを使用して呼び出すことができますT

メンバー関数コールバックへのポインターを取る関数を使用することも、関数ポインターと同様に、非常に簡単です。

 C my_c{2}; // aggregate initialization
 int a = 5;
 int b = C_foobar(a, my_c, &C::foo); // call C_foobar with pointer to foo as its callback

3. std::functionオブジェクト(ヘッダー<functional>

このstd::functionクラスは、呼び出し可能オブジェクトを格納、コピー、または呼び出すための多態性関数ラッパーです。

3.1 std::functionオブジェクト/タイプ表記の記述

std::function呼び出し可能オブジェクトを格納するオブジェクトのタイプは次のようになります。

std::function<return_type(parameter_type_1, parameter_type_2, parameter_type_3)>

// i.e. using the above function declaration of foo:
std::function<int(int)> stdf_foo = &foo;
// or C::foo:
std::function<int(const C&, int)> stdf_C_foo = &C::foo;

3.2コールバック呼び出し表記

クラスstd::functionoperator()、そのターゲットを呼び出すために使用できるものを定義しました。

int stdf_foobar (int x, std::function<int(int)> moo)
{
    return x + moo(x); // std::function moo called
}
// or 
int stdf_C_foobar (int x, C const &c, std::function<int(C const &, int)> moo)
{
    return x + moo(c, x); // std::function moo called using c and x
}

3.3コールバック使用表記と互換性のある型

std::function異なるタイプが渡され、暗黙的に変換することができるので、コールバック関数のポインタまたはメンバ関数へのポインタよりもより一般的であるstd::functionオブジェクト。

3.3.1関数ポインターとメンバー関数へのポインター

関数ポインタ

int a = 2;
int b = stdf_foobar(a, &foo);
// b == 6 ( 2 + (2+2) )

またはメンバー関数へのポインター

int a = 2;
C my_c{7}; // aggregate initialization
int b = stdf_C_foobar(a, c, &C::foo);
// b == 11 == ( 2 + (7+2) )

に使える。

3.3.2ラムダ式

ラムダ式からの名前のないクロージャはstd::functionオブジェクトに保存できます:

int a = 2;
int c = 3;
int b = stdf_foobar(a, [c](int x) -> int { return 7+c*x; });
// b == 15 ==  a + (7*c*a) == 2 + (7+3*2)

3.3.3 std::bind

式の結果をstd::bind渡すことができます。たとえば、パラメーターを関数ポインター呼び出しにバインドすることによって:

int foo_2 (int x, int y) { return 9*x + y; }
using std::placeholders::_1;

int a = 2;
int b = stdf_foobar(a, std::bind(foo_2, _1, 3));
// b == 23 == 2 + ( 9*2 + 3 )
int c = stdf_foobar(a, std::bind(foo_2, 5, _1));
// c == 49 == 2 + ( 9*5 + 2 )

メンバー関数へのポインターを呼び出すためのオブジェクトとしてオブジェクトをバインドすることもできます。

int a = 2;
C const my_c{7}; // aggregate initialization
int b = stdf_foobar(a, std::bind(&C::foo, my_c, _1));
// b == 1 == 2 + ( 2 + 7 )

3.3.4関数オブジェクト

適切なoperator()オーバーロードを持つクラスのオブジェクトも、std::functionオブジェクト内に格納できます。

struct Meow
{
  int y = 0;
  Meow(int y_) : y(y_) {}
  int operator()(int x) { return y * x; }
};
int a = 11;
int b = stdf_foobar(a, Meow{8});
// b == 99 == 11 + ( 8 * 11 )

3.4例

使用する関数ポインターの例を変更する std::function

void stdf_tranform_every_int(int * v, unsigned n, std::function<int(int)> fp)
{
  for (unsigned i = 0; i < n; ++i)
  {
    v[i] = fp(v[i]);
  }
}

(3.3を参照)私たちはそれを使用する可能性が高いので、その関数にずっと多くのユーティリティを与えます:

// using function pointer still possible
int a[5] = {1, 2, 3, 4, 5};
stdf_tranform_every_int(&a[0], 5, double_int);
// now a == {2, 4, 6, 8, 10};

// use it without having to write another function by using a lambda
stdf_tranform_every_int(&a[0], 5, [](int x) -> int { return x/2; });
// now a == {1, 2, 3, 4, 5}; again

// use std::bind :
int nine_x_and_y (int x, int y) { return 9*x + y; }
using std::placeholders::_1;
// calls nine_x_and_y for every int in a with y being 4 every time
stdf_tranform_every_int(&a[0], 5, std::bind(nine_x_and_y, _1, 4));
// now a == {13, 22, 31, 40, 49};

4.テンプレート化されたコールバックタイプ

テンプレートを使用すると、コールバックを呼び出すコードは、std::functionオブジェクトを使用するよりもさらに一般的になる可能性があります。

テンプレートはコンパイル時の機能であり、コンパイル時のポリモーフィズムのための設計ツールであることに注意してください。ランタイムダイナミック動作がコールバックを通じて実現される場合、テンプレートは役立ちますが、ランタイムダイナミックを誘発しません。

4.1テンプレートのコールバックの記述(タイプ表記)と呼び出し

一般化、つまり上記のstd_ftransform_every_intコードは、テンプレートを使用することでさらに実現できます。

template<class R, class T>
void stdf_transform_every_int_templ(int * v,
  unsigned const n, std::function<R(T)> fp)
{
  for (unsigned i = 0; i < n; ++i)
  {
    v[i] = fp(v[i]);
  }
}

コールバックタイプのさらに一般的な(そして最も簡単な)構文は、単純な、推定されるテンプレート化された引数です。

template<class F>
void transform_every_int_templ(int * v, 
  unsigned const n, F f)
{
  std::cout << "transform_every_int_templ<" 
    << type_name<F>() << ">\n";
  for (unsigned i = 0; i < n; ++i)
  {
    v[i] = f(v[i]);
  }
}

注:含まれている出力には、テンプレート化されたタイプについて推定されたタイプ名が印刷されFます。の実装はtype_nameこの投稿の最後に記載されています。

範囲の単項変換の最も一般的な実装は、標準ライブラリの一部、つまりstd::transform、反復型に関してテンプレート化されているです。

template<class InputIt, class OutputIt, class UnaryOperation>
OutputIt transform(InputIt first1, InputIt last1, OutputIt d_first,
  UnaryOperation unary_op)
{
  while (first1 != last1) {
    *d_first++ = unary_op(*first1++);
  }
  return d_first;
}

4.2テンプレート化されたコールバックと互換性のある型を使用した例

テンプレート化されたstd::functionコールバックメソッドの互換性のある型は、stdf_transform_every_int_templ上記の型と同じです(3.4を参照)。

ただし、テンプレートバージョンを使用すると、使用されるコールバックのシグネチャが少し変わる場合があります。

// Let
int foo (int x) { return 2+x; }
int muh (int const &x) { return 3+x; }
int & woof (int &x) { x *= 4; return x; }

int a[5] = {1, 2, 3, 4, 5};
stdf_transform_every_int_templ<int,int>(&a[0], 5, &foo);
// a == {3, 4, 5, 6, 7}
stdf_transform_every_int_templ<int, int const &>(&a[0], 5, &muh);
// a == {6, 7, 8, 9, 10}
stdf_transform_every_int_templ<int, int &>(&a[0], 5, &woof);

注:(std_ftransform_every_intテンプレート化されてfooいないバージョン。上記を参照)はで機能しますが、を使用しませんmuh

// Let
void print_int(int * p, unsigned const n)
{
  bool f{ true };
  for (unsigned i = 0; i < n; ++i)
  {
    std::cout << (f ? "" : " ") << p[i]; 
    f = false;
  }
  std::cout << "\n";
}

のプレーンテンプレートパラメータは、transform_every_int_templ呼び出し可能なすべてのタイプにすることができます。

int a[5] = { 1, 2, 3, 4, 5 };
print_int(a, 5);
transform_every_int_templ(&a[0], 5, foo);
print_int(a, 5);
transform_every_int_templ(&a[0], 5, muh);
print_int(a, 5);
transform_every_int_templ(&a[0], 5, woof);
print_int(a, 5);
transform_every_int_templ(&a[0], 5, [](int x) -> int { return x + x + x; });
print_int(a, 5);
transform_every_int_templ(&a[0], 5, Meow{ 4 });
print_int(a, 5);
using std::placeholders::_1;
transform_every_int_templ(&a[0], 5, std::bind(foo_2, _1, 3));
print_int(a, 5);
transform_every_int_templ(&a[0], 5, std::function<int(int)>{&foo});
print_int(a, 5);

上記のコードは以下を出力します:

1 2 3 4 5
transform_every_int_templ <int(*)(int)>
3 4 5 6 7
transform_every_int_templ <int(*)(int&)>
6 8 10 12 14
transform_every_int_templ <int& (*)(int&)>
9 11 13 15 17
transform_every_int_templ <main::{lambda(int)#1} >
27 33 39 45 51
transform_every_int_templ <Meow>
108 132 156 180 204
transform_every_int_templ <std::_Bind<int(*(std::_Placeholder<1>, int))(int, int)>>
975 1191 1407 1623 1839
transform_every_int_templ <std::function<int(int)>>
977 1193 1409 1625 1841

type_name 上記で使用した実装

#include <type_traits>
#include <typeinfo>
#include <string>
#include <memory>
#include <cxxabi.h>

template <class T>
std::string type_name()
{
  typedef typename std::remove_reference<T>::type TR;
  std::unique_ptr<char, void(*)(void*)> own
    (abi::__cxa_demangle(typeid(TR).name(), nullptr,
    nullptr, nullptr), std::free);
  std::string r = own != nullptr?own.get():typeid(TR).name();
  if (std::is_const<TR>::value)
    r += " const";
  if (std::is_volatile<TR>::value)
    r += " volatile";
  if (std::is_lvalue_reference<T>::value)
    r += " &";
  else if (std::is_rvalue_reference<T>::value)
    r += " &&";
  return r;
}

35
@BogeyJammer:気づいていない場合のために:答えは2つの部分に分かれています。1.小さな例を使用した「コールバック」の一般的な説明。2.さまざまな呼び出し可能オブジェクトの包括的なリストと、コールバックを使用してコードを記述する方法。詳細に掘り下げたり、回答全体を読んだりすることはできませんが、詳細なビューが必要ないという理由だけで、回答が効果的ではない、または「残酷にコピーされた」というわけではありません。トピックは「c ++コールバック」です。パート1でOPが問題なくても、パート2が役立つ場合があります。最初の部分については、-1ではなく、情報の欠如や建設的な批判を自由に指摘してください。
Pixelchemist 2016年

1
パート1は初心者向けではなく、十分に明確ではありません。何かを学ぶことができなかったと言っても、もっと建設的になることはできません。そして、パート2は要求されず、ページをフラッディングし、そのような詳細な情報が最初に検索される専用のドキュメントで一般的に見られるにもかかわらず、それが有用であると偽ったとしても問題外です。私は間違いなく反対投票を続けます。一票は個人的な意見を表すので、それを受け入れて尊重してください。
Bogey Jammer

24
@BogeyJammerプログラミングは初めてではありませんが、「モダンc ++」は初めてです。この答えは、コールバックが特にC ++で果たす役割について推論する必要がある正確なコンテキストを私に与えます。OPは複数の例を要求しなかったかもしれませんが、質問に対するすべての可能な解決策を列挙するために、愚か者の世界を教育するという終わりのない探求において、SOでは慣例です。それが本のように読みすぎる場合、私が提供できる唯一のアドバイスは、それらのいくつかを読んで少し練習することです。
dcow 2016

int b = foobar(a, foo); // call foobar with pointer to foo as callback、これはタイプミスですよね?fooこれがAFAIKを機能させるためのポインタである必要があります。
konoufo 2017年

@konoufo:[conv.func]C ++ 11標準の言う:「関数型Tの左辺値は、「ポインタへのT」型のprvalueに変換できます。結果は関数へのポインタです。「これは標準の変換であり、暗黙的に発生します。ここでは(もちろん)関数ポインタを使用できます。
Pixelchemist 2017年

160

コールバックを行うCの方法もあります。関数ポインター

//Define a type for the callback signature,
//it is not necessary, but makes life easier

//Function pointer called CallbackType that takes a float
//and returns an int
typedef int (*CallbackType)(float);  


void DoWork(CallbackType callback)
{
  float variable = 0.0f;

  //Do calculations

  //Call the callback with the variable, and retrieve the
  //result
  int result = callback(variable);

  //Do something with the result
}

int SomeCallback(float variable)
{
  int result;

  //Interpret variable

  return result;
}

int main(int argc, char ** argv)
{
  //Pass in SomeCallback to the DoWork
  DoWork(&SomeCallback);
}

ここで、クラスメソッドをコールバックとして渡したい場合、それらの関数ポインターへの宣言には、より複雑な宣言があります。例:

//Declaration:
typedef int (ClassName::*CallbackType)(float);

//This method performs work using an object instance
void DoWorkObject(CallbackType callback)
{
  //Class instance to invoke it through
  ClassName objectInstance;

  //Invocation
  int result = (objectInstance.*callback)(1.0f);
}

//This method performs work using an object pointer
void DoWorkPointer(CallbackType callback)
{
  //Class pointer to invoke it through
  ClassName * pointerInstance;

  //Invocation
  int result = (pointerInstance->*callback)(1.0f);
}

int main(int argc, char ** argv)
{
  //Pass in SomeCallback to the DoWork
  DoWorkObject(&ClassName::Method);
  DoWorkPointer(&ClassName::Method);
}

1
クラスメソッドの例にエラーがあります。呼び出しは:(instance。* callback)(1.0f)
CarlJohnson

指摘いただきありがとうございます。オブジェクトとオブジェクトポインターによる呼び出しを説明するために、両方を追加します。
Ramon Zarazua B. 2012

3
これには、コールバックがクラスごとに入力されるという点でstd :: tr1:functionの欠点があります。このため、呼び出しを実行するオブジェクトが、呼び出されるオブジェクトのクラスを認識していない場合、Cスタイルのコールバックを使用することは現実的ではありません。
Bleater、2013

どうすればtypedefコールバックタイプを使用せずにそれを行うことができますか?可能ですか?
トマーシュZato -復活モニカ

1
はい、できます。typedef読みやすくするための構文上の砂糖です。がなければtypedef、関数ポインターのDoWorkObjectの定義は次のようになりますvoid DoWorkObject(int (*callback)(float))。メンバーへのポインタは次のようになります。void DoWorkObject(int (ClassName::*callback)(float))
ラモンZarazua B.

68

スコット・マイヤーズは良い例を挙げています:

class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);

class GameCharacter
{
public:
  typedef std::function<int (const GameCharacter&)> HealthCalcFunc;

  explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
  : healthFunc(hcf)
  { }

  int healthValue() const { return healthFunc(*this); }

private:
  HealthCalcFunc healthFunc;
};

例はそれをすべて言うと思います。

std::function<> C ++コールバックを記述する「モダンな」方法です。


1
興味深いことに、SMはこの本をどの本に載せていますか?乾杯:)
sam-w

5
私はこれが古いことを知っていますが、これを始めようとしていて、セットアップ(mingw)で機能しなくなったため、GCCバージョン4.x未満を使用している場合、この方法はサポートされていません。私が使用している依存関係の一部は、gccバージョン4.0.1以降では多くの作業を行わないとコンパイルできません。そのため、正常に機能する古き良きC形式のコールバックの使用に悩まされています。
OzBarry 2012

38

コールバック関数ルーチンに渡され、それが渡されたルーチンによっていくつかの点で呼び出されるメソッドです。

これは、再利用可能なソフトウェアを作成するのに非常に役立ちます。たとえば、多くのオペレーティングシステムAPI(Windows APIなど)はコールバックを頻繁に使用します。

たとえば、フォルダ内のファイルを操作する場合、独自のルーチンを使用してAPI関数を呼び出すことができ、ルーチンは指定されたフォルダ内のファイルごとに1回実行されます。これにより、APIは非常に柔軟になります。


63
この答えは、平均的なプログラマが知らなかったことを実際に言っているわけではありません。他の多くの言語に精通しながら、C ++を学んでいます。一般的にどのようなコールバックでもかまいません。
トマーシュ・ザト-モニカを2014年

17

受け入れられた答えは非常に有用で、非常に包括的です。ただし、OPは

コールバック関数を書くための簡単な例を見たいのですが。

さあ、C ++ 11からstd::functionは、関数ポインタなどの必要はありません。

#include <functional>
#include <string>
#include <iostream>

void print_hashes(std::function<int (const std::string&)> hash_calculator) {
    std::string strings_to_hash[] = {"you", "saved", "my", "day"};
    for(auto s : strings_to_hash)
        std::cout << s << ":" << hash_calculator(s) << std::endl;    
}

int main() {
    print_hashes( [](const std::string& str) {   /** lambda expression */
        int result = 0;
        for (int i = 0; i < str.length(); i++)
            result += pow(31, i) * str.at(i);
        return result;
    });
    return 0;
}

この例は、どういうわけか現実的print_hashesです。ハッシュ関数の異なる実装で関数を呼び出したいので、この目的のために簡単なものを提供しました。文字列を受け取り、int(提供された文字列のハッシュ値)を返します。構文部分から覚えておく必要がstd::function<int (const std::string&)>あるのは、そのような関数を、それを呼び出す関数の入力引数として説明することだけです。


上記のすべての回答から、これによりコールバックとは何か、およびその使用方法を理解することができました。ありがとう。
Mehar Charan Sahai

@MeharCharanSahaiそれを聞いてうれしい:)あなたは大歓迎です。
ミルジェンミキック

9

C ++には、コールバック関数の明確な概念はありません。コールバックメカニズムは、多くの場合、関数ポインター、ファンクターオブジェクト、またはコールバックオブジェクトを介して実装されます。プログラマーは、コールバック機能を明示的に設計および実装する必要があります。

フィードバックに基づいて編集:

この回答は否定的なフィードバックを受け取っていますが、間違いではありません。私はどこから来たのかを説明するより良い仕事をするように努めます。

CおよびC ++には、コールバック関数を実装するために必要なものがすべて揃っています。コールバック関数を実装する最も一般的で簡単な方法は、関数の引数として関数ポインターを渡すことです。

ただし、コールバック関数と関数ポインターは同義ではありません。関数ポインタは言語メカニズムであり、コールバック関数はセマンティックな概念です。関数ポインターは、コールバック関数を実装する唯一の方法ではありません。ファンクターや、さまざまな仮想関数を使用することもできます。関数呼び出しをコールバックにするのは、関数を識別して呼び出すために使用されるメカニズムではなく、呼び出しのコンテキストとセマンティクスです。何かがコールバック関数であると言うことは、呼び出し元の関数と呼び出される特定の関数との間の通常の分離よりも大きい分離を意味し、呼び出し元と呼び出し先の間の概念的な結合が緩く、呼び出し元は呼び出されるものを明示的に制御します

たとえば、IFormatProviderの.NETドキュメントには、「GetFormat is a callback method」と書かれていますが、これはありふれたインターフェイスメソッドです。すべての仮想メソッド呼び出しがコールバック関数であると主張する人はいないと思います。GetFormatをコールバックメソッドにするのは、それが渡される方法や呼び出される方法の仕組みではなく、どのオブジェクトのGetFormatメソッドが呼び出されるかを選択する呼び出し元のセマンティクスです。

一部の言語には、通常はイベントとイベント処理に関連する、明示的なコールバックセマンティクスを持つ機能が含まれています。たとえば、C#には、コールバックの概念を中心に明示的に設計された構文とセマンティクスを持つイベントタイプがあります。Visual BasicにはHandles句があり、デリゲートまたは関数ポインターの概念を抽象化しながら、メソッドをコールバック関数として明示的に宣言します。これらの場合、コールバックの意味論的概念は言語自体に統合されています。

一方、CおよびC ++は、コールバック関数のセマンティックな概念をほとんど明示的に組み込みません。メカニズムはありますが、統合されたセマンティクスはありません。コールバック関数は問題なく実装できますが、明示的なコールバックセマンティクスを含むより洗練されたものを取得するには、QtがSignalsとSlotsで行ったことなど、C ++が提供するものの上に構築する必要があります。

簡単に言うと、C ++にはコールバックを実装するために必要なものがあり、多くの場合、関数ポインターを使用して非常に簡単かつ簡単にできます。それが持っていないものは、セマンティクスがコールバックに固有のキーワードと機能です(raiseemitHandlesevent + =など)。これらのタイプの要素を持つ言語から来ている場合、C ++のネイティブコールバックサポート去勢を感じるでしょう。


1
幸いにも、これはこのページにアクセスしたときに読んだ最初の回答ではありませんでした。それ以外の場合は、すぐに直帰しました。
ubugnu 2014

6

コールバック関数はC標準の一部であり、したがってC ++の一部でもあります。ただし、C ++を使用している場合は、代わりにオブザーバーパターンを使用することをお勧めします。http//en.wikipedia.org/wiki/Observer_pattern


1
コールバック関数は、必ずしも引数として渡された関数ポインターを介して関数を実行することと同義ではありません。定義によっては、コールバック関数という用語は、発生したばかりの何か、または何かが発生するはずの時間であることを他のコードに通知するという追加のセマンティクスを備えています。その観点から、コールバック関数はC標準の一部ではありませんが、標準の一部である関数ポインターを使用して簡単に実装できます。
ダリル14

3
「C標準の一部であり、したがってC ++の一部でもある。」これは典型的な誤解ですが、それでも誤解です:-)
限定的な贖罪

同意する必要があります。変更した場合に混乱が生じるだけなので、そのままにしておきます。関数ポインタ(!)は標準の一部であると言いました。それと異なることを言うと、私は同意しますが、誤解を招きます。
AudioDroid、2015

コールバック関数はどのように「C標準の一部」ですか?関数と関数へのポインターをサポートしているということは、コールバックを言語の概念として明確に正規化することを意味するとは思いません。さらに、前述のように、たとえそれが正確であっても、C ++に直接関連することはありません。そして、OPがC ++でコールバックを使用するように「いつ、どのように」と尋ねた場合(特に、あまりにも広義の質問ですが、それでも)は特に関係ありません。あなたの答えは、代わりに何かを行うためのリンクのみの警告です。
underscore_d 2017年

4

コールバック関数が他の関数に渡され、ある時点で呼び出されると述べている上記の定義を参照してください。

C ++では、コールバック関数がクラスメソッドを呼び出すことが望ましいです。これを行うと、メンバーデータにアクセスできます。コールバックを定義するCの方法を使用する場合は、それを静的メンバー関数にポイントする必要があります。これはあまり望ましくありません。

C ++でコールバックを使用する方法は次のとおりです。4つのファイルを想定します。各クラスの.CPP / .Hファイルのペア。クラスC1は、コールバックしたいメソッドを持つクラスです。C2はC1のメソッドを呼び出します。この例では、コールバック関数は、読者のために追加した1つのパラメーターを取ります。この例では、インスタンス化されて使用されているオブジェクトは示されていません。この実装の1つの使用例は、データを読み取り、一時スペースに格納するクラスと、データを後処理するクラスがある場合です。コールバック関数を使用すると、データのすべての行の読み取りに対して、コールバックはそれを処理できます。この手法により、必要な一時スペースのオーバーヘッドが削減されます。これは、大量の後処理が必要な大量のデータを返すSQLクエリに特に役立ちます。

/////////////////////////////////////////////////////////////////////
// C1 H file

class C1
{
    public:
    C1() {};
    ~C1() {};
    void CALLBACK F1(int i);
};

/////////////////////////////////////////////////////////////////////
// C1 CPP file

void CALLBACK C1::F1(int i)
{
// Do stuff with C1, its methods and data, and even do stuff with the passed in parameter
}

/////////////////////////////////////////////////////////////////////
// C2 H File

class C1; // Forward declaration

class C2
{
    typedef void (CALLBACK C1::* pfnCallBack)(int i);
public:
    C2() {};
    ~C2() {};

    void Fn(C1 * pThat,pfnCallBack pFn);
};

/////////////////////////////////////////////////////////////////////
// C2 CPP File

void C2::Fn(C1 * pThat,pfnCallBack pFn)
{
    // Call a non-static method in C1
    int i = 1;
    (pThat->*pFn)(i);
}

0

Boostのsignals2を使用すると、汎用のメンバー関数(テンプレートなし!)をスレッドセーフな方法でサブスクライブできます。

例:ドキュメントビュー信号を使用して、柔軟なドキュメントビューアーキテクチャを実装できます。ドキュメントには、各ビューが接続できる信号が含まれます。次のDocumentクラスは、複数のビューをサポートする単純なテキストドキュメントを定義します。すべてのビューが接続される単一の信号を保存することに注意してください。

class Document
{
public:
    typedef boost::signals2::signal<void ()>  signal_t;

public:
    Document()
    {}

    /* Connect a slot to the signal which will be emitted whenever
      text is appended to the document. */
    boost::signals2::connection connect(const signal_t::slot_type &subscriber)
    {
        return m_sig.connect(subscriber);
    }

    void append(const char* s)
    {
        m_text += s;
        m_sig();
    }

    const std::string& getText() const
    {
        return m_text;
    }

private:
    signal_t    m_sig;
    std::string m_text;
};

次に、ビューの定義を開始できます。次のTextViewクラスは、ドキュメントテキストの単純なビューを提供します。

class TextView
{
public:
    TextView(Document& doc): m_document(doc)
    {
        m_connection = m_document.connect(boost::bind(&TextView::refresh, this));
    }

    ~TextView()
    {
        m_connection.disconnect();
    }

    void refresh() const
    {
        std::cout << "TextView: " << m_document.getText() << std::endl;
    }
private:
    Document&               m_document;
    boost::signals2::connection  m_connection;
};

0

受け入れられた答えは包括的ですが、質問に関連していますが、ここに簡単な例を示します。ずっと前に書いたコードがあった。私はツリーを順番に(左ノード、ルートノード、右ノード)トラバースしたいと思い、1つのノードに到達するたびに任意の関数を呼び出してすべてを実行できるようにしたいと考えました。

void inorder_traversal(Node *p, void *out, void (*callback)(Node *in, void *out))
{
    if (p == NULL)
        return;
    inorder_traversal(p->left, out, callback);
    callback(p, out); // call callback function like this.
    inorder_traversal(p->right, out, callback);
}


// Function like bellow can be used in callback of inorder_traversal.
void foo(Node *t, void *out = NULL)
{
    // You can just leave the out variable and working with specific node of tree. like bellow.
    // cout << t->item;
    // Or
    // You can assign value to out variable like below
    // Mention that the type of out is void * so that you must firstly cast it to your proper out.
    *((int *)out) += 1;
}
// This function use inorder_travesal function to count the number of nodes existing in the tree.
void number_nodes(Node *t)
{
    int sum = 0;
    inorder_traversal(t, &sum, foo);
    cout << sum;
}

 int main()
{

    Node *root = NULL;
    // What These functions perform is inserting an integer into a Tree data-structure.
    root = insert_tree(root, 6);
    root = insert_tree(root, 3);
    root = insert_tree(root, 8);
    root = insert_tree(root, 7);
    root = insert_tree(root, 9);
    root = insert_tree(root, 10);
    number_nodes(root);
}

1
それは質問にどのように答えますか?
Rajan Sharma

受け入れられた答えが正しく包括的であることを知っているので、一般的に言っておくべきことはもうないと思います。しかし、私はコールバック関数の使用例を投稿します。
Ehsan Ahmadi
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.