Funkcje zwrotne w C ++

303

Kiedy i jak używasz funkcji zwrotnej w C ++?

EDYCJA:
Chciałbym zobaczyć prosty przykład napisania funkcji zwrotnej.

CPX
źródło
[To] ( thispointer.com/… ) bardzo dobrze wyjaśnia podstawy funkcji oddzwaniania i łatwo ją zrozumieć.
Anurag Singh

Odpowiedzi:

449

Uwaga: Większość odpowiedzi dotyczy wskaźników funkcji, co jest jedną z możliwości uzyskania logiki „wywołania zwrotnego” w C ++, ale jak na razie nie jest to najbardziej korzystna.

Co to są callbacki (?) I dlaczego ich używać (!)

Oddzwonienie to wywoływalne (patrz niżej) akceptowane przez klasę lub funkcję, używane do dostosowania bieżącej logiki w zależności od tego wywołania zwrotnego.

Jednym z powodów używania wywołań zwrotnych jest pisanie kodu ogólnego, który jest niezależny od logiki wywoływanej funkcji i może być ponownie użyty z różnymi wywołaniami zwrotnymi.

Wiele funkcji biblioteki standardowych algorytmów <algorithm>wykorzystuje wywołania zwrotne. Na przykład for_eachalgorytm stosuje jednoargumentowe wywołanie zwrotne do każdego elementu w zakresie iteratorów:

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

które można wykorzystać do zwiększenia, a następnie wydrukowania wektora, przekazując odpowiednie kallasze, na przykład:

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

który drukuje

5 6.2 8 9.5 11.2

Innym zastosowaniem wywołań zwrotnych jest powiadamianie dzwoniących o określonych zdarzeniach, które umożliwia pewną elastyczność czasu statycznego / kompilacji.

Osobiście korzystam z lokalnej biblioteki optymalizacji, która korzysta z dwóch różnych wywołań zwrotnych:

  • Pierwsze wywołanie zwrotne jest wywoływane, jeśli wymagana jest wartość funkcji i gradient oparty na wektorze wartości wejściowych (wywołanie logiczne: wyznaczanie wartości funkcji / wyprowadzanie gradientu).
  • Drugie wywołanie zwrotne jest wywoływane raz dla każdego kroku algorytmu i odbiera pewne informacje o zbieżności algorytmu (wywołanie zwrotne powiadomienia).

Tak więc projektant biblioteki nie jest odpowiedzialny za decydowanie o tym, co dzieje się z informacjami przekazywanymi programiście za pośrednictwem wywołania zwrotnego powiadomienia i nie musi się martwić, jak faktycznie określić wartości funkcji, ponieważ są one dostarczane przez wywołanie logiczne. Poprawienie tych rzeczy jest zadaniem użytkownika biblioteki i sprawia, że ​​biblioteka jest wąska i bardziej ogólna.

Ponadto wywołania zwrotne mogą umożliwić zachowanie dynamicznego środowiska wykonawczego.

Wyobraź sobie jakąś klasę silnika gry, która ma uruchamianą funkcję, za każdym razem, gdy użytkownik naciska przycisk na klawiaturze i zestaw funkcji kontrolujących twoje zachowanie w grze. Za pomocą wywołań zwrotnych możesz (ponownie) zdecydować w czasie wykonywania, które działanie zostanie podjęte.

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

Tutaj funkcja key_pressedwykorzystuje wywołania zwrotne zapisane w actionscelu uzyskania pożądanego zachowania po naciśnięciu określonego klawisza. Jeśli gracz zdecyduje się zmienić przycisk skoku, silnik może zadzwonić

game_core_instance.update_keybind(newly_selected_key, &player_jump);

i w ten sposób zmień zachowanie połączenia key_pressed( do którego dzwoni player_jump), gdy ten przycisk zostanie naciśnięty następnym razem w grze.

Co można wywoływać w C ++ (11)?

Zobacz pojęcia C ++: na żądanie cppreference, aby uzyskać bardziej formalny opis.

Funkcję oddzwaniania można realizować na kilka sposobów w C ++ (11), ponieważ można wywoływać kilka różnych rzeczy * :

  • Wskaźniki funkcji (w tym wskaźniki funkcji składowych)
  • std::function przedmioty
  • Wyrażenia lambda
  • Wiąż wyrażenia
  • Obiekty funkcji (klasy z przeciążonym operatorem wywołania funkcji operator())

* Uwaga: Wskaźnik do elementów danych jest również możliwy do wywołania, ale żadna funkcja nie jest w ogóle wywoływana.

Kilka ważnych sposobów szczegółowego zapisywania wywołań zwrotnych

  • X.1 „Zapisywanie” wywołania zwrotnego w tym poście oznacza składnię do deklarowania i nazwania typu wywołania zwrotnego.
  • X.2 „Wywołanie” wywołanie zwrotne odnosi się do składni wywoływania tych obiektów.
  • X.3 „Używanie” wywołania zwrotnego oznacza składnię podczas przekazywania argumentów do funkcji za pomocą wywołania zwrotnego.

Uwaga: Począwszy od C ++ 17, f(...)można napisać takie wywołanie, std::invoke(f, ...)które obsługuje również wskaźnik do wielkości elementu.

1. Wskaźniki funkcji

Wskaźnik funkcji jest „najprostszym” (pod względem ogólności; pod względem czytelności prawdopodobnie najgorszym) typem, który może mieć wywołanie zwrotne.

Miejmy prostą funkcję foo:

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

1.1 Pisanie notacji wskaźnik / typ funkcji

Typu wskaźnik funkcja ma notacji

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

gdzie będzie wyglądał nazwany typ wskaźnika funkcji

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;

usingDeklaracja daje nam możliwość, aby rzeczy trochę bardziej czytelne, ponieważ typedefza f_int_tmożna również zapisać jako:

using f_int_t = int(*)(int);

Gdzie (przynajmniej dla mnie) jest to wyraźniejsze f_int_t jest alias nowego typu, a rozpoznanie typu wskaźnika funkcji jest również łatwiejsze

Deklaracja funkcji wykorzystująca wywołanie zwrotne typu wskaźnika funkcji będzie:

// 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 Notacja połączeń oddzwaniania

Notacja wywołania jest zgodna z prostą składnią wywołania funkcji:

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 Notacja dotycząca oddzwaniania i kompatybilne typy

Funkcja zwrotna, która przyjmuje wskaźnik funkcji, może zostać wywołana za pomocą wskaźników funkcji.

Korzystanie z funkcji, która pobiera wywołanie zwrotne wskaźnika funkcji, jest dość proste:

 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 Przykład

Można napisać funkcję, która nie zależy od działania wywołania zwrotnego:

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

tam, gdzie to możliwe, mogą być zwrotne

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

używane jak

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. Wskaźnik do funkcji składowej

Wskaźnik do funkcji składowej (jakiejś klasy C) jest specjalnym typem (a nawet bardziej złożonym) wskaźnika funkcji, który wymaga obiektu typu Cdo działania.

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

2.1 Pisanie wskaźnika do notacji funkcji / typu elementu

Wskaźnik zarejestrował typu funkcyjnego dla pewnej klasy Tma notacji

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

gdzie nazwany wskaźnik do funkcji składowej będzie wyglądał analogicznie do wskaźnika funkcji:

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;

Przykład: Zadeklarowanie funkcji przyjmującej wskaźnik do wywołania zwrotnego funkcji członka jako jeden z jej argumentów:

// 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 Notacja połączeń oddzwaniania

Funkcję wskaźnika do elementu członkowskiego Cmożna wywołać w odniesieniu do obiektu typu Cza pomocą operacji dostępu do elementu na zdereferencyjnym wskaźniku. Uwaga: wymagany nawias!

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
}

Uwaga: Jeśli wskaźnik do Cjest dostępny, jego składnia jest równoważna (gdzie wskaźnik do również Cmusi zostać usunięty):

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 Notacja użycia oddzwaniania i kompatybilne typy

Funkcja zwrotna przyjmująca wskaźnik funkcji składowej klasy Tmoże być wywoływana za pomocą wskaźnika funkcji składowej klasy T.

Użycie funkcji, która przenosi wskaźnik do wywołania zwrotnego funkcji elementu, jest - podobnie jak wskaźniki funkcji - całkiem proste:

 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::functionobiekty (nagłówek<functional> )

std::functionKlasa jest polimorficzny funkcja otoki do przechowywania, kopiowania lub invoke callables.

3.1 Pisanie std::functionnotacji obiektowej / typu

Typ std::functionobiektu przechowującego wywołanie wygląda następująco:

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 Notacja połączeń oddzwaniania

Klasa std::functionjest operator()zdefiniowana, które mogą być wykorzystane do wywołania swój cel.

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 Notacja dotycząca oddzwaniania i kompatybilne typy

std::functionZwrotna jest bardziej ogólne niż wskaźników funkcji lub wskaźnik do funkcji składowej ponieważ różne typy mogą być przekazywane i niejawnie przekształcony w std::functionobiekt.

3.3.1 Wskaźniki funkcji i wskaźniki funkcji składowych

Wskaźnik funkcji

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

lub wskaźnik do funkcji składowej

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

może być użyty.

3.3.2 Wyrażenia lambda

Nienazwane zamknięcie wyrażenia lambda może być przechowywane w std::functionobiekcie:

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::bindwyrażenia

Wynik std::bindwyrażenia można przekazać. Na przykład przez powiązanie parametrów z wywołaniem wskaźnika funkcji:

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 )

Gdzie również obiekty mogą być powiązane jako obiekt do wywołania wskaźnika do funkcji składowych:

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 Obiekty funkcyjne

Obiekty klas o odpowiednim operator()przeciążeniu mogą być również przechowywane wewnątrz std::functionobiektu.

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 Przykład

Zmiana przykładowego wskaźnika funkcji do użycia 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]);
  }
}

daje o wiele większą użyteczność tej funkcji, ponieważ (patrz 3.3) mamy więcej możliwości korzystania z niej:

// 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. Szablon zwrotny typu zwrotnego

Za pomocą szablonów kod wywołujący wywołanie zwrotne może być nawet bardziej ogólny niż używanie std::functionobiektów.

Należy pamiętać, że szablony są funkcją czasu kompilacji i są narzędziem do projektowania polimorfizmu w czasie kompilacji. Jeśli zachowanie dynamiczne w środowisku wykonawczym ma zostać osiągnięte poprzez wywołania zwrotne, szablony pomogą, ale nie wywołają dynamiki środowiska wykonawczego.

4.1 Pisanie (notacje typów) i wywoływanie zwrotnych szablonów

Uogólnienie, tj. std_ftransform_every_intKod z góry jeszcze bardziej, można osiągnąć za pomocą szablonów:

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

z jeszcze bardziej ogólną (jak również najłatwiejszą) składnią dla typu wywołania zwrotnego będącego prostym argumentem szablonowym:

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

Uwaga: Dołączone dane wyjściowe wypisują nazwę typu wydedukowaną dla typu szablonowego F. Realizacja type_namejest podana na końcu tego postu.

Najbardziej ogólna implementacja jednoargumentowej transformacji zakresu jest częścią standardowej biblioteki, a mianowicie std::transform, która jest również wzorowana w odniesieniu do typów iterowanych.

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 Przykłady użycia oddzwaniania szablonowego i kompatybilnych typów

Typy kompatybilne dla matrycowej std::functionmetody wywołania zwrotnego stdf_transform_every_int_templsą identyczne z wyżej wymienionymi typami (patrz 3.4).

Jednak w wersji z szablonem podpis używanego wywołania zwrotnego może się nieco zmienić:

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

Uwaga: std_ftransform_every_int(wersja bez szablonów; patrz wyżej) działa, fooale nie używa 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";
}

Prostym parametrem szablonu transform_every_int_templmoże być każdy możliwy typ wywoływalny.

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

Powyższy kod drukuje:

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 implementacja zastosowana powyżej

#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
źródło
35
@BogeyJammer: W przypadku, gdy nie zauważyłeś: Odpowiedź składa się z dwóch części. 1. Ogólne wyjaśnienie „wywołań zwrotnych” z małym przykładem. 2. Pełna lista różnych wywołań i sposobów pisania kodu za pomocą wywołań zwrotnych. Możesz nie zagłębiać się w szczegóły lub przeczytać całą odpowiedź, ale tylko dlatego, że nie chcesz szczegółowego widoku, nie jest tak, że odpowiedź jest nieskuteczna lub „brutalnie skopiowana”. Temat to „wywołania zwrotne c ++”. Nawet jeśli część 1 jest odpowiednia dla OP, inni mogą uznać część 2 za przydatną. Zachęcamy do zwrócenia uwagi na brak informacji lub konstruktywną krytykę pierwszej części zamiast -1.
Pixelchemist
1
Część 1 nie jest wystarczająco przyjazna dla początkujących. Nie mogę być bardziej konstruktywny, mówiąc, że nie udało mi się czegoś nauczyć. A część 2 nie została poproszona o zalanie strony i nie wchodzi w rachubę, nawet jeśli udajesz, że jest przydatna, mimo że często znajduje się w dedykowanej dokumentacji, w której szuka się tak szczegółowych informacji. Zdecydowanie trzymam głos. Jeden głos reprezentuje osobistą opinię, więc proszę ją zaakceptować i uszanować.
Bogey Jammer
24
@BogeyJammer Nie jestem nowy w programowaniu, ale jestem nowy w „modern c ++”. Ta odpowiedź daje mi dokładny kontekst, który muszę uzasadnić na temat roli, jaką odgrywają oddzwaniania, w szczególności w c ++. OP mógł nie poprosić o wiele przykładów, ale zwyczajowo na SO, w niekończącym się dążeniu do edukacji świata głupców, wymienia się wszystkie możliwe rozwiązania pytania. Jeśli brzmi to jak książka, jedyną radą, jaką mogę zaoferować, jest poćwiczyć, czytając kilka z nich .
dcow
int b = foobar(a, foo); // call foobar with pointer to foo as callback, to jest literówka, prawda? foopowinien być wskaźnikiem, aby działało AFAIK.
konoufo
@konoufo: [conv.func]standardu C ++ 11 mówi: „ Wartość funkcji typu T można przekonwertować na wartość typu„ wskaźnik na T. ” Wynikiem jest wskaźnik do funkcji. ”Jest to standardowa konwersja i jako taka zachodzi niejawnie. Można tutaj (oczywiście) użyć wskaźnika funkcji.
Pixelchemist,
160

Istnieje również sposób wywołania zwrotnego w języku C: wskaźniki funkcji

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

Teraz, jeśli chcesz przekazać metody klas jako wywołania zwrotne, deklaracje do tych wskaźników funkcji mają bardziej złożone deklaracje, na przykład:

//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.
źródło
1
Wystąpił błąd w przykładzie metody klasy. Wywołanie powinno być: (instancja. *
Oddzwanianie
Dziękuję za zwrócenie na to uwagi. Dodam oba, aby zilustrować wywoływanie przez obiekt, i poprzez wskaźnik obiektu.
Ramon Zarazua B.
3
Ma to tę wadę, że funkcja std :: tr1: polega na tym, że wywołanie zwrotne jest typowane dla poszczególnych klas; sprawia to, że niepraktyczne jest stosowanie wywołań zwrotnych w stylu C, gdy obiekt wykonujący wywołanie nie zna klasy obiektu, który ma zostać wywołany.
bleater
Jak mogę to zrobić bez typedefwprowadzania typu wywołania zwrotnego? Czy to w ogóle możliwe?
Tomáš Zato - Przywróć Monikę
1
Tak, możesz. typedefto tylko cukier składniowy, aby był bardziej czytelny. Bez typedefdefinicja DoWorkObject dla wskaźników funkcji będzie: void DoWorkObject(int (*callback)(float)). Wskaźniki członków to:void DoWorkObject(int (ClassName::*callback)(float))
Ramon Zarazua B.
68

Scott Meyers podaje ładny przykład:

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

Myślę, że przykład mówi wszystko.

std::function<> to „nowoczesny” sposób pisania wywołań zwrotnych C ++.

Karl von Moor
źródło
1
Z braku zainteresowania, w której książce SM podaje ten przykład? Pozdrawiam :)
sam-w
5
Wiem, że to jest stare, ale ponieważ prawie zacząłem to robić i ostatecznie nie działało to w mojej konfiguracji (mingw), jeśli używasz GCC w wersji <4.x, ta metoda nie jest obsługiwana. Niektóre zależności, których używam, nie będą się kompilować bez dużego nakładu pracy w wersji gcc> = 4.0.1, więc utknąłem przy użyciu starych, dobrych wywołań zwrotnych w stylu C, które działają dobrze.
OzBarry
38

Funkcja zwrotna to metoda przekazywana do procedury i wywoływana w pewnym momencie przez procedurę, do której jest przekazywana.

Jest to bardzo przydatne do tworzenia oprogramowania wielokrotnego użytku. Na przykład wiele interfejsów API systemu operacyjnego (takich jak Windows API) intensywnie wykorzystuje wywołania zwrotne.

Na przykład, jeśli chcesz pracować z plikami w folderze - możesz wywołać funkcję API z własną procedurą, a twoja procedura uruchamia się raz na plik w określonym folderze. Dzięki temu interfejs API jest bardzo elastyczny.

Reed Copsey
źródło
63
Ta odpowiedź naprawdę nie oznacza, że ​​programista mówi cokolwiek, czego nie wiedział. Uczę się C ++, znając wiele innych języków. To, co w ogóle jest oddzwanianiem, nie dotyczy mnie.
Tomáš Zato - Przywróć Monikę
17

Przyjęta odpowiedź jest bardzo przydatna i dość wyczerpująca. Jednak OP stwierdza

Chciałbym zobaczyć prosty przykład napisania funkcji zwrotnej.

Więc proszę, od C ++ 11 masz, std::functionwięc nie ma potrzeby używania wskaźników funkcji i podobnych rzeczy:

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

Ten przykład jest jakoś prawdziwy, ponieważ chcesz wywołać funkcję print_hashesz różnymi implementacjami funkcji skrótu, w tym celu podałem prostą. Odbiera ciąg, zwraca wartość int (wartość skrótu podanego ciągu) i wszystko, co musisz zapamiętać z części składni, to std::function<int (const std::string&)>opisanie takiej funkcji jako argument wejściowy funkcji, która ją wywoła.

Miljen Mikic
źródło
spośród wszystkich powyższych odpowiedzi ta pozwoliła mi zrozumieć, czym są wywołania zwrotne i jak z nich korzystać. dzięki.
Mehar Charan Sahai
@MeharCharanSahai Cieszę się, że to słyszę :) Nie ma za co.
Miljen Mikic
9

W C ++ nie ma wyraźnej koncepcji funkcji zwrotnej. Mechanizmy zwrotne są często implementowane za pomocą wskaźników funkcji, obiektów funktorów lub obiektów zwrotnych. Programiści muszą jawnie zaprojektować i wdrożyć funkcjonalność wywołania zwrotnego.

Edytuj na podstawie opinii:

Pomimo negatywnej opinii, którą otrzymała ta odpowiedź, nie jest zła. Spróbuję lepiej wyjaśnić, skąd pochodzę.

C i C ++ mają wszystko, czego potrzebujesz, aby zaimplementować funkcje zwrotne. Najczęstszym i najprostszym sposobem implementacji funkcji zwrotnej jest przekazanie wskaźnika funkcji jako argumentu funkcji.

Jednak funkcje oddzwaniania i wskaźniki funkcji nie są synonimami. Wskaźnik funkcji jest mechanizmem językowym, a funkcja zwrotna jest pojęciem semantycznym. Wskaźniki funkcji nie są jedynym sposobem na wdrożenie funkcji wywołania zwrotnego - możesz także użyć funktorów, a nawet funkcji wirtualnych odmiany ogrodu. To, co powoduje, że wywołanie funkcji jest wywołaniem zwrotnym, to nie mechanizm używany do identyfikacji i wywoływania funkcji, ale kontekst i semantyka wywołania. Powiedzenie czegoś jest funkcją wywołania zwrotnego oznacza większą niż normalnie separację między funkcją wywołującą a wywoływaną określoną funkcją, luźniejsze połączenie koncepcyjne między dzwoniącym a odbiorcą, przy czym dzwoniący ma wyraźną kontrolę nad tym, co zostanie wywołane.

Na przykład dokumentacja .NET dla IFormatProvider mówi, że „GetFormat jest metodą wywołania zwrotnego” , nawet jeśli jest to zwykła metoda interfejsu. Nie sądzę, aby ktokolwiek argumentował, że wszystkie wirtualne wywołania metod są funkcjami zwrotnymi. To, co sprawia, że ​​GetFormat jest metodą wywołania zwrotnego, to nie mechanika tego, w jaki sposób jest ona przekazywana lub wywoływana, ale semantyka wybierania numeru wywołującego, która metoda GetFormat obiektu zostanie wywołana.

Niektóre języki zawierają funkcje z wyraźną semantyką wywołania zwrotnego, zwykle związane ze zdarzeniami i obsługą zdarzeń. Na przykład C # ma typ zdarzenia ze składnią i semantyką wyraźnie zaprojektowaną wokół koncepcji wywołań zwrotnych. Visual Basic ma klauzulę Handles , która jawnie deklaruje metodę jako funkcję zwrotną, jednocześnie abstrahując od koncepcji delegatów lub wskaźników funkcji. W takich przypadkach semantyczna koncepcja wywołania zwrotnego jest zintegrowana z samym językiem.

Z kolei C i C ++ nie osadzają semantycznej koncepcji funkcji zwrotnych prawie tak wyraźnie. Istnieją mechanizmy, nie ma zintegrowanej semantyki. Możesz dobrze zaimplementować funkcje zwrotne, ale aby uzyskać coś bardziej zaawansowanego, w tym wyraźną semantykę zwrotną, musisz zbudować ją na podstawie tego, co zapewnia C ++, na przykład tego, co Qt zrobił z ich sygnałami i gniazdami .

W skrócie, C ++ ma to, czego potrzebujesz do implementacji wywołań zwrotnych, często dość łatwo i trywialnie za pomocą wskaźników funkcji. To, czego nie ma, to słowa kluczowe i funkcje, których semantyka jest specyficzna dla wywołań zwrotnych, takich jak podbicie , emisja , uchwyty , zdarzenie + = itp. Jeśli pochodzisz z języka zawierającego takie typy elementów, natywne wsparcie zwrotne w C ++ będzie się czuł neutralny.

Darryl
źródło
1
na szczęście nie była to pierwsza odpowiedź, którą przeczytałem, odwiedzając tę ​​stronę, w przeciwnym razie od razu bym się odrzucił!
ubugnu
6

Funkcje zwrotne są częścią standardu C, a zatem również częścią C ++. Ale jeśli pracujesz z C ++, sugerowałbym zamiast tego użyć wzorca obserwatora : http://en.wikipedia.org/wiki/Observer_pattern

AudioDroid
źródło
1
Funkcje zwrotne niekoniecznie są synonimami wykonywania funkcji za pomocą wskaźnika funkcji, który został przekazany jako argument. W niektórych definicjach termin funkcja zwrotna przenosi dodatkową semantykę powiadamiania jakiegoś innego kodu o czymś, co właśnie się wydarzyło, lub jest to czas, kiedy coś powinno się wydarzyć. Z tej perspektywy funkcja wywołania zwrotnego nie jest częścią standardu C, ale może być łatwo zaimplementowana za pomocą wskaźników funkcji, które są częścią standardu.
Darryl,
3
„część standardu C, a zatem także część C ++.” Jest to typowe nieporozumienie, ale jednak nieporozumienie :-)
Ograniczone Zadośćuczynienie
Muszę się zgodzić. Zostawię go takim, jakim jest, ponieważ spowoduje to więcej zamieszania, jeśli teraz go zmienię. Chciałem powiedzieć, że wskaźnik funkcji (!) Jest częścią standardu. Mówienie czegoś innego - zgadzam się - jest mylące.
AudioDroid
W jaki sposób funkcje zwrotne są „częścią standardu C”? Nie sądzę, że fakt, że obsługuje funkcje i wskaźniki do funkcji, oznacza, że ​​w szczególności kanonizuje wywołania zwrotne jako koncepcję językową. Poza tym, jak wspomniano, nie byłoby to bezpośrednio związane z C ++, nawet gdyby było dokładne. Jest to szczególnie nieistotne, gdy OP zapytał „kiedy i jak” używać wywołań zwrotnych w C ++ (kiepskie, zbyt szerokie pytanie, ale mimo to), a twoja odpowiedź to tylko napomnienie, aby zrobić coś innego.
underscore_d
4

Zobacz powyższą definicję, w której stwierdzono, że funkcja zwrotna jest przekazywana do innej funkcji i w pewnym momencie jest wywoływana.

W C ++ pożądane jest, aby funkcje zwrotne wywoływały metodę klas. Po wykonaniu tej czynności masz dostęp do danych członka. Jeśli użyjesz C do zdefiniowania wywołania zwrotnego, będziesz musiał skierować go do statycznej funkcji składowej. To nie jest bardzo pożądane.

Oto, w jaki sposób można korzystać z wywołań zwrotnych w C ++. Załóż 4 pliki. Para plików .CPP / .H dla każdej klasy. Klasa C1 to klasa z metodą, którą chcemy oddzwonić. C2 odwołuje się do metody C1. W tym przykładzie funkcja zwrotna przyjmuje 1 parametr, który dodałem dla czytelników. W przykładzie nie pokazano żadnych obiektów, które są tworzone i wykorzystywane. Jednym z przypadków użycia tej implementacji jest sytuacja, gdy masz jedną klasę, która odczytuje i przechowuje dane w tymczasowej przestrzeni, a drugą, która publikuje dane. Dzięki funkcji wywołania zwrotnego, dla każdego odczytanego wiersza danych, wywołanie zwrotne może następnie przetworzyć. Ta technika odcina narzut wymaganej tymczasowej przestrzeni. Jest to szczególnie przydatne w przypadku zapytań SQL, które zwracają dużą ilość danych, które następnie muszą zostać przetworzone.

/////////////////////////////////////////////////////////////////////
// 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
źródło
0

Boost za signals2 pozwala subskrybować rodzajowe funkcji składowych (bez szablonów!) Iw sposób THREADSAFE.

Przykład: Sygnały widoku dokumentu można wykorzystać do implementacji elastycznych architektur widoku dokumentu. Dokument będzie zawierał sygnał, z którym każdy widok może się połączyć. Następująca klasa Document definiuje prosty dokument tekstowy, który obsługuje wiele widoków. Zauważ, że przechowuje pojedynczy sygnał, do którego zostaną podłączone wszystkie widoki.

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

Następnie możemy zacząć definiować widoki. Następująca klasa TextView zapewnia prosty widok tekstu dokumentu.

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
źródło
0

Przyjęta odpowiedź jest wyczerpująca, ale związana z pytaniem, chcę tylko tutaj podać prosty przykład. Miałem kod, który napisałem dawno temu. chciałem przemierzać drzewo w uporządkowany sposób (lewy węzeł, następnie węzeł główny, a następnie prawy) i za każdym razem, gdy docieram do jednego węzła, chciałem móc wywołać dowolną funkcję, aby mogła zrobić wszystko.

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
źródło
1
jak odpowiada na pytanie?
Rajan Sharma
wiesz, że zaakceptowana odpowiedź jest poprawna i wyczerpująca i myślę, że ogólnie nie ma już nic do powiedzenia. ale zamieszczam przykład mojego użycia funkcji zwrotnych.
Ehsan Ahmadi