1. ホーム
  2. c++

[解決済み】C++のコールバック関数

2022-03-24 19:28:27

質問

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

EDITです。

コールバック関数の簡単な書き方の例を教えてほしいです。

どのように解決するのですか?

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

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

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

コールバックを使用する理由の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;
}

は、例えば適切な callable を渡すことで、まずベクトルをインクリメントし、次にプリントするために使用することができます。

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 このボタンを押すと、次回からこのボタンが押せるようになります。

何が callables C++(11)の場合?

参照 C++の概念です。callable の cppreference を参照すると、より正式な説明があります。

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

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

<サブ * 注:データメンバへのポインタも同様に呼び出し可能ですが、関数は全く呼び出されません。

いくつかの重要な書き方 コールバック 詳しく

  • X.1 コールバックの書き方 この投稿では、コールバックの型を宣言して名前を付ける構文を意味します。
  • X.2 "Calling" a callbackは、それらのオブジェクトを呼び出すためのシンタックスを指します。
  • X.3 "Using" a callback とは、コールバックを使って関数に引数を渡すときの構文です。

注:C++17では、以下のような呼び出しがあります。 f(...) は次のように書くことができます。 std::invoke(f, ...) であり、メンバーケースへのポインタも扱える。

1. 関数ポインタ

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

簡単な関数を考えてみましょう。 foo :

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

1.1 関数ポインタの書き方/型表記法

A 関数ポインタ型 は、以下のような表記になります。

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 メンバ関数へのポインタの書き方/型表記法

A メンバ関数型へのポインタ あるクラスに対して 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 クラスは、callableを保存、コピー、呼び出すための多相関数ラッパーです。

3.1 std::function オブジェクト/タイプ記法

の型は std::function オブジェクトに格納されているcallableは、次のようになります。

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