Fungsi panggilan balik di C ++

303

Di C ++, kapan dan bagaimana Anda menggunakan fungsi panggilan balik?

EDIT:
Saya ingin melihat contoh sederhana untuk menulis fungsi panggilan balik.

cpx
sumber
[Ini] ( thispointer.com/... ) menjelaskan dasar-dasar tentang fungsi panggilan balik dengan sangat baik dan mudah untuk memahami konsep.
Anurag Singh

Jawaban:

449

Catatan: Sebagian besar jawaban mencakup pointer fungsi yang merupakan salah satu kemungkinan untuk mencapai logika "panggilan balik" dalam C ++, tetapi pada hari ini bukan yang paling menguntungkan yang saya pikir.

Apa itu callback (?) Dan mengapa menggunakannya (!)

Callback adalah callable (lihat lebih jauh ke bawah) yang diterima oleh kelas atau fungsi, yang digunakan untuk menyesuaikan logika saat ini tergantung pada callback itu.

Salah satu alasan untuk menggunakan callback adalah untuk menulis kode generik yang independen dari logika pada fungsi yang dipanggil dan dapat digunakan kembali dengan callback yang berbeda.

Banyak fungsi pustaka algoritma standar <algorithm>menggunakan panggilan balik. Misalnya for_eachalgoritme menerapkan panggilan balik unary ke setiap item dalam rentang iterator:

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

yang dapat digunakan untuk kenaikan pertama dan kemudian mencetak vektor dengan melewatkan perangkat yang sesuai misalnya:

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

yang mencetak

5 6.2 8 9.5 11.2

Aplikasi lain dari callback adalah pemberitahuan penelepon dari peristiwa tertentu yang memungkinkan sejumlah fleksibilitas waktu statis / kompilasi.

Secara pribadi, saya menggunakan perpustakaan pengoptimalan lokal yang menggunakan dua panggilan balik berbeda:

  • Callback pertama dipanggil jika nilai fungsi dan gradien berdasarkan vektor nilai input diperlukan (logic callback: penentuan nilai fungsi / derivasi gradien).
  • Callback kedua dipanggil sekali untuk setiap langkah algoritma dan menerima informasi tertentu tentang konvergensi algoritma (notifikasi callback).

Dengan demikian, perancang perpustakaan tidak bertanggung jawab untuk memutuskan apa yang terjadi dengan informasi yang diberikan kepada programmer melalui callback notifikasi dan dia tidak perlu khawatir tentang bagaimana sebenarnya menentukan nilai fungsi karena mereka disediakan oleh callback logika. Memperbaiki hal-hal itu adalah tugas karena pengguna perpustakaan dan menjaga perpustakaan langsing dan lebih umum.

Selanjutnya, panggilan balik dapat mengaktifkan perilaku runtime dinamis.

Bayangkan beberapa jenis kelas mesin gim yang memiliki fungsi yang dipecat, setiap kali pengguna menekan tombol di keyboard-nya dan serangkaian fungsi yang mengontrol perilaku gim Anda. Dengan panggilan balik Anda dapat (kembali) memutuskan pada saat runtime tindakan mana yang akan diambil.

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

Di sini fungsi key_pressedmenggunakan panggilan balik yang disimpan actionsuntuk mendapatkan perilaku yang diinginkan ketika tombol tertentu ditekan. Jika pemain memilih untuk mengubah tombol untuk melompat, mesin dapat memanggil

game_core_instance.update_keybind(newly_selected_key, &player_jump);

dan dengan demikian mengubah perilaku panggilan ke key_pressed(panggilan mana player_jump) setelah tombol ini ditekan pada saat ingame berikutnya.

Apa itu callable di C ++ (11)?

Lihat konsep C ++: Callable on cppreference untuk deskripsi yang lebih formal.

Fungsionalitas panggilan balik dapat diwujudkan dalam beberapa cara dalam C ++ (11) karena beberapa hal yang berbeda ternyata dapat dipanggil * :

  • Pointer fungsi (termasuk pointer ke fungsi anggota)
  • std::function benda
  • Ekspresi Lambda
  • Mengikat ekspresi
  • Objek fungsi (kelas dengan operator panggilan fungsi yang kelebihan beban operator())

* Catatan: Pointer ke anggota data juga dapat dipanggil tetapi tidak ada fungsi yang dipanggil sama sekali.

Beberapa cara penting untuk menulis panggilan balik secara terperinci

  • X.1 "Menulis" panggilan balik dalam posting ini berarti sintaks untuk menyatakan dan memberi nama jenis panggilan balik.
  • X.2 "Memanggil" panggilan balik mengacu pada sintaks untuk memanggil objek-objek itu.
  • X.3 "Menggunakan" panggilan balik berarti sintaks ketika meneruskan argumen ke suatu fungsi menggunakan panggilan balik.

Catatan: Pada C ++ 17, panggilan seperti f(...)dapat ditulis sebagai std::invoke(f, ...)yang juga menangani pointer ke case anggota.

1. Pointer fungsi

Pointer fungsi adalah yang paling sederhana (dalam hal generalitas; dalam hal keterbacaan bisa dibilang yang terburuk) jenis callback dapat memiliki.

Mari kita fungsi sederhana foo:

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

1.1 Menulis fungsi pointer / notasi jenis

Sebuah tipe fungsi pointer memiliki notasi

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

di mana jenis penunjuk fungsi bernama akan terlihat seperti

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;

The usingdeklarasi memberi kita pilihan untuk membuat hal-hal sedikit lebih mudah dibaca, karena typedefuntuk f_int_tjuga dapat ditulis sebagai:

using f_int_t = int(*)(int);

Di mana (setidaknya bagi saya) lebih jelas bahwa itu f_int_tadalah tipe baru alias dan pengenalan tipe fungsi pointer juga lebih mudah

Dan deklarasi fungsi yang menggunakan callback dari tipe pointer fungsi adalah:

// 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 Notasi panggilan balik

Notasi panggilan mengikuti sintaks panggilan fungsi sederhana:

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 Callback menggunakan notasi dan tipe yang kompatibel

Fungsi callback mengambil pointer fungsi dapat disebut menggunakan pointer fungsi.

Menggunakan fungsi yang mengambil callback fungsi pointer agak sederhana:

 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 Contoh

Fungsi yang ditulis yang tidak bergantung pada cara kerja callback:

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

di mana panggilan balik mungkin bisa

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

digunakan seperti

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. Pointer ke fungsi anggota

Sebuah pointer ke fungsi anggota (dari beberapa kelas C) adalah tipe khusus dari (dan bahkan lebih kompleks) pointer fungsi yang membutuhkan objek tipe Cuntuk beroperasi.

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

2.1 Menulis pointer ke notasi fungsi / tipe anggota

Sebuah pointer ke tipe fungsi anggota untuk beberapa kelas Tmemiliki notasi

// 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)

di mana penunjuk bernama untuk fungsi anggota akan -di analogi dengan fungsi penunjuk- terlihat seperti ini:

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;

Contoh: Mendeklarasikan fungsi yang mengambil pointer ke fungsi panggilan balik anggota sebagai salah satu argumennya:

// 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 Notasi panggilan balik

Fungsi pointer ke anggota Cdapat dipanggil, sehubungan dengan objek tipe Cdengan menggunakan operasi akses anggota pada pointer dereferenced. Catatan: Diperlukan Parenthesis!

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
}

Catatan: Jika sebuah pointer ke Ctersedia, sintaksinya setara (di mana pointer juga Charus didereferensi):

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 Notasi penggunaan panggilan balik dan jenis yang kompatibel

Fungsi callback mengambil pointer fungsi anggota kelas Tdapat dipanggil menggunakan pointer fungsi anggota kelas T.

Menggunakan fungsi yang mengambil pointer ke fungsi anggota callback adalah -di analogi dengan fungsi pointer- cukup sederhana juga:

 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::functionobjek (header <functional>)

The std::functionkelas adalah fungsi wrapper polimorfik untuk menyimpan, menyalin atau Invoke callables.

3.1 Menulis std::functionnotasi objek / tipe

Jenis std::functionobjek menyimpan callable terlihat seperti:

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 Notasi panggilan balik

Kelas std::functiontelah operator()ditentukan yang dapat digunakan untuk menjalankan targetnya.

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 Notasi penggunaan panggilan balik dan jenis yang kompatibel

The std::functioncallback lebih generik dari pointer fungsi atau pointer ke fungsi anggota karena jenis yang berbeda dapat berlalu dan secara implisit diubah menjadi std::functionobjek.

3.3.1 Pointer fungsi dan pointer ke fungsi anggota

Pointer fungsi

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

atau pointer ke fungsi anggota

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

dapat digunakan.

3.3.2 Ekspresi Lambda

Penutupan tanpa ekspresi dari ekspresi lambda dapat disimpan di std::functionobjek:

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::bindekspresi

Hasil dari std::bindekspresi dapat dilewati. Misalnya dengan mengikat parameter ke panggilan fungsi pointer:

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 )

Di mana juga objek dapat diikat sebagai objek untuk pemanggilan pointer ke fungsi anggota:

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 Objek fungsi

Objek kelas yang memiliki operator()kelebihan yang tepat dapat disimpan di dalam std::functionobjek juga.

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 Contoh

Mengubah contoh penunjuk fungsi untuk digunakan 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]);
  }
}

memberi jauh lebih banyak utilitas untuk fungsi itu karena (lihat 3.3) kami memiliki lebih banyak kemungkinan untuk menggunakannya:

// 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. Jenis panggilan balik templated

Menggunakan templat, kode yang memanggil panggilan balik bisa lebih umum daripada menggunakan std::functionobjek.

Perhatikan bahwa templat adalah fitur waktu kompilasi dan merupakan alat desain untuk polimorfisme waktu kompilasi. Jika perilaku dinamis runtime ingin dicapai melalui callback, template akan membantu tetapi mereka tidak akan menginduksi dinamika runtime.

4.1 Menulis (mengetik notasi) dan memanggil panggilan balik templated

Generalisasi yaitu std_ftransform_every_intkode dari atas lebih jauh dapat dicapai dengan menggunakan templat:

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

dengan sintaks yang lebih umum (dan juga termudah) untuk tipe panggilan balik menjadi argumen templated sederhana yang akan dideduksi:

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

Catatan: Output yang disertakan mencetak nama jenis yang disimpulkan untuk jenis templated F. Implementasi type_namediberikan pada akhir posting ini.

Implementasi paling umum untuk transformasi unary dari jangkauan adalah bagian dari perpustakaan standar, yaitu std::transform, yang juga disesuaikan dengan jenis yang diulang.

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 Contoh menggunakan callback templated dan tipe yang kompatibel

Tipe yang kompatibel untuk std::functionmetode callback templated stdf_transform_every_int_templidentik dengan tipe yang disebutkan di atas (lihat 3.4).

Namun, menggunakan versi templated, tanda tangan dari panggilan balik yang digunakan dapat berubah sedikit:

// 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);

Catatan: std_ftransform_every_int(versi non templated; lihat di atas) berfungsi dengan footetapi tidak menggunakan 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";
}

Parameter templated polos dari transform_every_int_templdapat setiap jenis yang bisa dipanggil.

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

Kode di atas mencetak:

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 implementasi yang digunakan di atas

#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;
}
Pixelchemist
sumber
35
@ BogeyJammer: Jika Anda belum memperhatikan: Jawabannya memiliki dua bagian. 1. Penjelasan umum "panggilan balik" dengan contoh kecil. 2. Daftar komprehensif berbagai callable dan cara menulis kode menggunakan callback. Anda dipersilakan untuk tidak menggali detail atau membaca seluruh jawaban tetapi hanya karena Anda tidak ingin tampilan yang terperinci, itu bukan berarti jawabannya tidak efektif atau "disalin secara brutal". Topiknya adalah "c ++ callbacks". Bahkan jika bagian 1 ok untuk OP, orang lain mungkin menemukan bagian 2 berguna. Jangan ragu menunjukkan kurangnya informasi atau kritik yang membangun untuk bagian pertama alih-alih -1.
Pixelchemist
1
Bagian 1 tidak ramah pemula dan cukup jelas. Saya tidak bisa lebih konstruktif dengan mengatakan itu tidak berhasil mempelajari saya sesuatu. Dan bagian 2, tidak diminta, membanjiri halaman dan keluar dari pertanyaan meskipun Anda berpura-pura memiliki kegunaan meskipun faktanya itu umumnya ditemukan dalam dokumentasi khusus di mana informasi terperinci seperti itu dicari di tempat pertama. Saya pasti menyimpan downvote. Satu suara mewakili pendapat pribadi, jadi mohon terima dan hormati.
Bogey Jammer
24
@ BogeyJammer Saya bukan orang baru dalam pemrograman tetapi saya baru mengenal "modern c ++". Jawaban ini memberi saya konteks yang tepat saya perlu alasan tentang peran callback bermain, khususnya, c ++. OP mungkin tidak meminta banyak contoh, tetapi merupakan kebiasaan pada SO, dalam upaya yang tidak pernah berakhir untuk mendidik dunia orang-orang bodoh, untuk menyebutkan semua solusi yang mungkin untuk sebuah pertanyaan. Jika bunyinya terlalu banyak seperti buku, satu-satunya saran yang bisa saya tawarkan adalah berlatih sedikit dengan membaca beberapa di antaranya .
dcow
int b = foobar(a, foo); // call foobar with pointer to foo as callback, ini salah cetak kan? fooharus menjadi penunjuk untuk ini bekerja AFAIK.
konoufo
@konoufo: [conv.func]dari standar C ++ 11 mengatakan: "Nilai fungsi tipe T dapat dikonversi ke nilai awal tipe" pointer to T. " Hasilnya adalah penunjuk ke fungsi. "Ini adalah konversi standar dan karenanya terjadi secara implisit. Orang bisa (tentu saja) menggunakan pointer fungsi di sini.
Pixelchemist
160

Ada juga cara C untuk melakukan panggilan balik: pointer fungsi

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

Sekarang jika Anda ingin meneruskan metode kelas sebagai panggilan balik, deklarasi ke pointer fungsi tersebut memiliki deklarasi yang lebih kompleks, contoh:

//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);
}
Ramon Zarazua B.
sumber
1
Ada kesalahan dalam contoh metode kelas. Doa harus: (misalnya. *
Panggilan
Terima kasih telah menunjukkannya. Saya akan menambahkan keduanya untuk mengilustrasikan pemanggilan melalui objek, dan melalui penunjuk objek.
Ramon Zarazua B.
3
Ini memiliki kelemahan dari fungsi std :: tr1: di mana callback diketik per-kelas; ini membuatnya tidak praktis untuk menggunakan panggilan balik gaya-C ketika objek yang melakukan panggilan tidak mengetahui kelas objek yang akan dipanggil.
bleater
Bagaimana saya bisa melakukannya tanpa typedeftipe panggilan balik? Apakah itu mungkin?
Tomáš Zato - Reinstate Monica
1
Ya kamu bisa. typedefhanyalah gula sintaksis agar lebih mudah dibaca. Tanpa typedef, definisi DoWorkObject untuk fungsi pointer akan menjadi: void DoWorkObject(int (*callback)(float)). Untuk petunjuk anggota adalah:void DoWorkObject(int (ClassName::*callback)(float))
Ramon Zarazua B.
68

Scott Meyers memberikan contoh yang bagus:

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

Saya pikir contoh mengatakan semuanya.

std::function<> adalah cara "modern" untuk menulis panggilan balik C ++.

Karl von Moor
sumber
1
Karena ketertarikan, di buku mana SM memberikan contoh ini? Cheers :)
sam-w
5
Saya tahu ini sudah tua, tetapi karena saya hampir mulai melakukan ini dan akhirnya tidak berfungsi pada pengaturan saya (mingw), jika Anda menggunakan versi GCC <4.x, metode ini tidak didukung. Beberapa dependensi yang saya gunakan tidak dapat dikompilasi tanpa banyak pekerjaan di versi gcc> = 4.0.1, jadi saya terjebak dengan menggunakan callback gaya C kuno yang baik, yang bekerja dengan baik.
OzBarry
38

Sebuah fungsi Callback adalah metode yang dilewatkan ke rutinitas, dan disebut di beberapa titik oleh rutinitas yang itu berlalu.

Ini sangat berguna untuk membuat perangkat lunak yang dapat digunakan kembali. Misalnya, banyak API sistem operasi (seperti Windows API) banyak menggunakan panggilan balik.

Misalnya, jika Anda ingin bekerja dengan file dalam folder - Anda dapat memanggil fungsi API, dengan rutinitas Anda sendiri, dan rutin Anda dijalankan sekali per file dalam folder yang ditentukan. Ini memungkinkan API menjadi sangat fleksibel.

Reed Copsey
sumber
63
Jawaban ini benar-benar tidak programmer rata-rata mengatakan sesuatu yang tidak dia ketahui. Saya belajar C ++ sambil terbiasa dengan banyak bahasa lain. Apa panggilan balik secara umum tidak menjadi perhatian saya.
Tomáš Zato - Reinstate Monica
17

Jawaban yang diterima sangat bermanfaat dan cukup komprehensif. Namun, OP menyatakan

Saya ingin melihat contoh sederhana untuk menulis fungsi panggilan balik.

Jadi di sini Anda mulai, dari C ++ 11 yang Anda miliki std::functionsehingga tidak perlu pointer fungsi dan hal-hal serupa:

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

Contoh ini benar-benar nyata, karena Anda ingin memanggil fungsi print_hashesdengan implementasi fungsi hash yang berbeda, untuk tujuan ini saya berikan yang sederhana. Ini menerima string, mengembalikan int (nilai hash dari string yang disediakan), dan semua yang perlu Anda ingat dari bagian sintaks adalah std::function<int (const std::string&)>yang menggambarkan fungsi seperti argumen input dari fungsi yang akan memanggilnya.

Miljen Mikic
sumber
dari semua jawaban di atas, yang satu ini membuat saya mengerti apa itu panggilan balik dan bagaimana menggunakannya. Terima kasih.
Mehar Charan Sahai
@MeharCharanSahai Senang mendengarnya :) Terima kasih.
Miljen Mikic
9

Tidak ada konsep eksplisit fungsi panggilan balik di C ++. Mekanisme panggilan balik sering diterapkan melalui pointer fungsi, objek functor, atau objek panggilan balik. Programmer harus secara eksplisit mendesain dan mengimplementasikan fungsi callback.

Edit berdasarkan umpan balik:

Terlepas dari umpan balik negatif yang diterima jawaban ini, itu tidak salah. Saya akan mencoba melakukan pekerjaan yang lebih baik untuk menjelaskan dari mana saya berasal.

C dan C ++ memiliki semua yang Anda butuhkan untuk mengimplementasikan fungsi panggilan balik. Cara paling umum dan sepele untuk mengimplementasikan fungsi callback adalah dengan melewatkan pointer fungsi sebagai argumen fungsi.

Namun, fungsi panggilan balik dan fungsi pointer tidak sama. Pointer fungsi adalah mekanisme bahasa, sedangkan fungsi callback adalah konsep semantik. Pointer fungsi bukan satu-satunya cara untuk mengimplementasikan fungsi callback - Anda juga dapat menggunakan functors dan bahkan berbagai fungsi virtual kebun. Apa yang membuat suatu fungsi memanggil panggilan balik bukanlah mekanisme yang digunakan untuk mengidentifikasi dan memanggil fungsi, tetapi konteks dan semantik dari panggilan tersebut. Mengatakan sesuatu adalah fungsi callback menyiratkan pemisahan yang lebih besar dari normal antara fungsi pemanggilan dan fungsi spesifik yang dipanggil, penggabungan konseptual yang lebih longgar antara pemanggil dan callee, dengan pemanggil memiliki kontrol eksplisit atas apa yang dipanggil.

Sebagai contoh, dokumentasi .NET untuk IFormatProvider mengatakan bahwa "GetFormat adalah metode panggilan balik" , meskipun itu hanya metode antarmuka run-of-the-mill. Saya tidak berpikir ada orang yang berpendapat bahwa semua panggilan metode virtual adalah fungsi panggilan balik. Apa yang menjadikan GetFormat sebagai metode panggilan balik bukanlah mekanisme bagaimana hal itu dilewatkan atau dipanggil, tetapi semantik penelepon memilih metode GetFormat objek mana yang akan dipanggil.

Beberapa bahasa menyertakan fitur dengan semantik panggilan balik eksplisit, biasanya terkait dengan acara dan penanganan acara. Misalnya, C # memiliki tipe acara dengan sintaks dan semantik yang dirancang secara eksplisit di sekitar konsep panggilan balik. Visual Basic memiliki klausa Handles -nya , yang secara eksplisit mendeklarasikan metode menjadi fungsi callback sambil mengabstraksi konsep delegasi atau pointer fungsi. Dalam kasus ini, konsep semantik panggilan balik diintegrasikan ke dalam bahasa itu sendiri.

C dan C ++, di sisi lain, tidak menanamkan konsep semantik fungsi callback hampir secara eksplisit. Mekanismenya ada, semantik terintegrasi tidak. Anda dapat mengimplementasikan fungsi panggilan balik dengan baik, tetapi untuk mendapatkan sesuatu yang lebih canggih yang mencakup semantik panggilan balik eksplisit Anda harus membangunnya di atas apa yang disediakan C ++, seperti apa yang dilakukan Qt dengan Sinyal dan Slot mereka .

Singkatnya, C ++ memiliki apa yang Anda butuhkan untuk mengimplementasikan panggilan balik, seringkali cukup mudah dan sepele menggunakan pointer fungsi. Apa yang tidak dimilikinya adalah kata kunci dan fitur yang semantiknya khusus untuk panggilan balik, seperti kenaikan , buang , Pegangan , acara + = , dll. Jika Anda berasal dari bahasa dengan jenis elemen tersebut, panggilan balik asli mendukung dalam C ++ akan merasa dikebiri.

Darryl
sumber
1
untungnya ini bukan jawaban pertama yang saya baca ketika saya mengunjungi halaman ini, kalau tidak, saya akan segera bangkit!
ubugnu
6

Fungsi panggilan balik adalah bagian dari standar C, oleh karena itu juga merupakan bagian dari C ++. Tetapi jika Anda bekerja dengan C ++, saya sarankan Anda menggunakan pola pengamat saja: http://en.wikipedia.org/wiki/Observer_pattern

AudioDroid
sumber
1
Fungsi panggilan balik tidak harus identik dengan menjalankan fungsi melalui penunjuk fungsi yang disahkan sebagai argumen. Dengan beberapa definisi, fungsi panggilan balik istilah membawa semantik tambahan memberitahukan beberapa kode lain dari sesuatu yang baru saja terjadi, atau itu adalah waktu bahwa sesuatu harus terjadi. Dari perspektif itu, fungsi callback bukan bagian dari standar C, tetapi dapat dengan mudah diimplementasikan menggunakan pointer fungsi, yang merupakan bagian dari standar.
Darryl
3
"Bagian dari standar C, oleh karena itu juga bagian dari C ++." Ini adalah kesalahpahaman yang khas, tetapi kesalahpahaman tetap :-)
Penebusan Terbatas
Saya harus setuju. Saya akan membiarkannya apa adanya, karena itu hanya akan menyebabkan lebih banyak kebingungan jika saya mengubahnya sekarang. Saya bermaksud mengatakan bahwa fungsi pointer (!) Adalah bagian dari standar. Mengatakan sesuatu yang berbeda dari itu - saya setuju - menyesatkan.
AudioDroid
Dengan cara apa fungsi callback "bagian dari standar C"? Saya tidak berpikir fakta bahwa itu mendukung fungsi dan pointer ke fungsi berarti secara khusus mengkanonisasi panggilan balik sebagai konsep bahasa. Selain itu, seperti yang disebutkan, itu tidak akan secara langsung relevan dengan C ++ bahkan jika itu akurat. Dan itu terutama tidak relevan ketika OP bertanya "kapan dan bagaimana" untuk menggunakan panggilan balik dalam C ++ (pertanyaan yang lemah, terlalu luas, tapi tetap saja), dan jawaban Anda adalah peringatan hanya tautan untuk melakukan sesuatu yang berbeda.
underscore_d
4

Lihat definisi di atas di mana ia menyatakan bahwa fungsi callback dilewatkan ke beberapa fungsi lain dan pada titik tertentu disebut.

Dalam C ++ diinginkan untuk memiliki fungsi panggilan balik memanggil metode kelas. Ketika Anda melakukan ini, Anda memiliki akses ke data anggota. Jika Anda menggunakan cara C mendefinisikan panggilan balik, Anda harus mengarahkannya ke fungsi anggota statis. Ini sangat tidak diinginkan.

Inilah cara Anda dapat menggunakan panggilan balik di C ++. Asumsikan 4 file. Sepasang file .CPP / .H untuk setiap kelas. Kelas C1 adalah kelas dengan metode yang ingin kita panggil kembali. C2 memanggil kembali ke metode C1. Dalam contoh ini fungsi callback mengambil 1 parameter yang saya tambahkan demi pembaca. Contoh tidak menunjukkan objek apa pun yang dipakai dan digunakan. Satu kasus penggunaan untuk implementasi ini adalah ketika Anda memiliki satu kelas yang membaca dan menyimpan data ke dalam ruang sementara dan yang lain yang memposting memproses data. Dengan fungsi panggilan balik, untuk setiap baris data, baca panggilan balik kemudian dapat memprosesnya. Teknik ini memotong overhead ruang sementara yang diperlukan. Ini sangat berguna untuk query SQL yang mengembalikan sejumlah besar data yang kemudian harus diproses pasca.

/////////////////////////////////////////////////////////////////////
// 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);
}
Gravy Jones
sumber
0

Signal2 Boost memungkinkan Anda untuk berlangganan fungsi anggota generik (tanpa templat!) Dan dengan cara yang aman.

Contoh: Sinyal Tampilan Dokumen dapat digunakan untuk mengimplementasikan arsitektur Document-View yang fleksibel. Dokumen akan berisi sinyal yang dapat dihubungkan oleh setiap tampilan. Kelas Dokumen berikut mendefinisikan dokumen teks sederhana yang mendukung tampilan mulitple. Perhatikan bahwa ia menyimpan sinyal tunggal yang akan menghubungkan semua tampilan.

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

Selanjutnya, kita dapat mulai mendefinisikan tampilan. Kelas TextView berikut memberikan tampilan sederhana dari teks dokumen.

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;
};
crizCraig
sumber
0

Jawaban yang diterima komprehensif tetapi terkait dengan pertanyaan saya hanya ingin memberikan contoh sederhana di sini. Saya punya kode yang sudah lama saya tulis. saya ingin melintasi pohon dengan cara berurutan (simpul kiri lalu root-simpul kemudian kanan-simpul) dan setiap kali saya mencapai satu Node saya ingin dapat memanggil fungsi sewenang-wenang sehingga bisa melakukan segalanya.

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);
}
Ehsan Ahmadi
sumber
1
bagaimana cara menjawab pertanyaan?
Rajan Sharma
Anda tahu jawaban yang diterima benar dan komprehensif dan saya pikir tidak ada lagi yang bisa dikatakan secara umum. tetapi saya memposting contoh penggunaan fungsi callback saya.
Ehsan Ahmadi