C++のコールバック関数 質問する

C++のコールバック関数 質問する

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

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

ベストアンサー1

注: 回答のほとんどは、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

コールバックのもう 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 the 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 a 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;

宣言により、 for を次のように記述することもできるusingため、もう少し読みやすくするオプションが提供されます。typedeff_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 a 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 a 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 a 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 be 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.1std::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.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.3std::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;
}
        

おすすめ記事