web-dev-qa-db-ja.com

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

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

編集:
コールバック関数を書く簡単な例を見たいと思います。

259
cpx

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

コールバックとは何ですか?

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

コールバックを使用する理由の1つは、呼び出される関数のロジックから独立しており、さまざまなコールバックで再利用できるgenericコードを記述することです。

標準アルゴリズムライブラリ<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

コールバックのもう1つの用途は、特定のイベントの呼び出し元への通知です。これにより、一定量の静的/コンパイル時の柔軟性が得られます。

個人的には、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_pressedactionsに保存されているコールバックを使用して、特定のキーが押されたときに目的の動作を取得します。プレイヤーがジャンプ用のボタンを変更することを選択した場合、エンジンは呼び出すことができます

game_core_instance.update_keybind(newly_selected_key, &player_jump);

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

C++(11)のcallablesとは何ですか?

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

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

  • 関数ポインター(メンバー関数へのポインターを含む)
  • 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宣言は、f_int_ttypedefも次のように記述できるため、物事を少し読みやすくするオプションを提供します。

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コールバック使用表記法と互換性のあるタイプ

クラスTのメンバー関数ポインターを使用するコールバック関数は、クラス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::functionには、ターゲットを呼び出すために使用できるoperator()が定義されています。

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.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.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 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.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;
}
380
Pixelchemist

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);
}
151

Scott Meyersがいい例を挙げます。

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++コールバックを書くための「現代的」な方法です。

66
Karl von Moor

A コールバック関数 は、ルーチンに渡されるメソッドで、渡されたルーチンによって呼び出されます。

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

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

37
Reed Copsey

受け入れられた答えは非常に有用で、そして非常に包括的です。しかし、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&)>だけで、それを呼び出す関数の入力引数として記述します。

13
Miljen Mikic

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

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

この回答が寄せられた否定的なフィードバックにもかかわらず、それは誤りではありません。私は私がどこから来たのかを説明するより良い仕事をしようとします。

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

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

たとえば、 IFormatProvider の.NETドキュメントには、 "GetFormatはコールバックメソッドです" があります。それは単なる実行インタフェースメソッドです。すべての仮想メソッド呼び出しがコールバック関数であると誰かが主張することはないと思います。 GetFormatをコールバックメソッドにするのは、それがどのように渡されたり呼び出されたりするかのメカニズムではなく、どのオブジェクトのGetFormatメソッドが呼び出されるかを選ぶ呼び出し側のセマンティクスです。

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

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

一言で言えば、C++にはコールバックを実装するために必要なものが揃っています。多くの場合、関数ポインタを使用することで、非常に簡単かつ自明になります。それが持っていないものはその意味論がコールバックに特定であるキーワードと特徴、例えば raise emit です。 event + = などを処理します。これらのタイプの要素を持つ言語から来ている場合は、C++でのネイティブコールバックのサポートは中途半端です。

8
Darryl

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

6
AudioDroid

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

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);
}
4
Gravy Jones

Boostの signals2 を使用すると、一般的なメンバー関数を(テンプレートなしで)購読でき、スレッドセーフな方法で使用できます。

例:Document-View Signalsを使用して、柔軟なDocument-Viewアーキテクチャを実装できます。文書には、各ビューが接続できるシグナルが含まれています。次の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
crizCraig

私はずっと前に書いたコードを持っています。私は順不同でツリーをたどりたかったのですが(左ノード、次にルートノード、そして右ノード)、そして一つのノードにたどり着くたびに、私はそれがすべてができるように任意の関数を呼び出すことができたかったのです。

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);
}
0
Ehsan Ahmadi