Функції зворотного дзвінка в C ++


303

Коли і як ви використовуєте функцію зворотного дзвінка у програмі C ++?

EDIT:
Я хотів би побачити простий приклад написання функції зворотного виклику.


[Це] ( thispointer.com/… ) дуже добре пояснює основи функцій зворотного виклику та їх легко зрозуміти.
Анураг Сінгх

Відповіді:


449

Примітка: Більшість відповідей охоплюють покажчики функцій, що є однією можливістю досягти логіки "зворотного виклику" в C ++, але на сьогоднішній день не найсприятливіший.

Що таке зворотні дзвінки (?) Та навіщо їх використовувати (!)

Як передзвонити є викликається (див далі вниз) прийнятий класом або функцією, використовуваним для настройки поточної логіки в залежності від цього зворотного виклику.

Однією з причин використання зворотних викликів є написання загального коду, який не залежить від логіки в викликаній функції і може бути повторно використаний з різними зворотними зворотами.

Багато функцій бібліотеки стандартних алгоритмів <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

Ще одне застосування зворотних викликів - повідомлення сповіщувачів про певні події, що забезпечує певну кількість статичної / гнучкої компіляції часу.

Особисто я використовую локальну бібліотеку оптимізації, яка використовує два різні зворотні виклики:

  • Перший зворотний виклик викликається, якщо потрібне значення функції та градієнт, заснований на векторі вхідних значень (логічний зворотний виклик: визначення значення функції / градієнт).
  • Другий зворотний виклик викликається один раз для кожного кроку алгоритму і отримує певну інформацію про конвергенцію алгоритму (зворотний виклик сповіщення).

Таким чином, дизайнер бібліотеки не відповідає за прийняття рішення про те, що відбувається з інформацією, яка надається програмісту за допомогою зворотного виклику сповіщень, і він не повинен турбуватися про те, як фактично визначити значення функцій, оскільки вони забезпечуються логічним зворотним дзвінком. Правильне налаштування цих завдань є завданням, що належить користувачеві бібліотеки, і зберігає бібліотеку стрункою та загальною.

Крім того, зворотні виклики можуть включати динамічну поведінку під час виконання.

Уявіть собі якийсь клас ігрового двигуна, який має функцію, яка спрацьовує, кожного разу, коли користувачі натискають кнопку на його клавіатурі та набір функцій, які керують вашою ігровою поведінкою. За допомогою зворотних викликів ви можете (повторно) вирішити під час виконання, які дії будуть вжиті.

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) після натискання цієї кнопки наступного разу.

Що таке дзвінки в 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 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 Написання вказівника на нотацію функції / типу члена

Показник на тип функції члена для деякого класу 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;

Приклад: Оголошення функції, що приймає покажчик на зворотний виклик функції члена, як один із його аргументів:

// C_foobar having an argument named moo of type pointer to member function of C
// where the callback returns int taking int as its argument
// also needs an object of type c
int C_foobar (int x, C const &c, int (C::*moo)(int));
// can equivalently declared using the typedef above:
int C_foobar (int x, C const &c, f_C_int_t moo);

2.2 Позначення виклику зворотного дзвінка

Функція вказівника на член-член Cможе бути викликана стосовно типу об'єкта C, використовуючи операції доступу членів на відміненому вказівнику. Примітка: необхідні парентези!

int C_foobar (int x, C const &c, int (C::*moo)(int))
{
    return x + (c.*moo)(x); // function pointer moo called for object c using argument x
}
// analog
int C_foobar (int x, C const &c, f_C_int_t moo)
{
    return x + (c.*moo)(x); // function pointer moo called for object c using argument x
}

Примітка: Якщо вказівник на Cдоступний, синтаксис еквівалентний (де вказівник також Cповинен бути відмінений):

int C_foobar_2 (int x, C const * c, int (C::*meow)(int))
{
    if (!c) return x;
    // function pointer meow called for object *c using argument x
    return x + ((*c).*meow)(x); 
}
// or equivalent:
int C_foobar_2 (int x, C const * c, int (C::*meow)(int))
{
    if (!c) return x;
    // function pointer meow called for object *c using argument x
    return x + (c->*meow)(x); 
}

2.3 Використання позначень зворотного дзвінка та сумісних типів

Функцію зворотного дзвінка, що приймає вказівник функції члена класу, Tможна викликати, використовуючи вказівник функції класу класу T.

Використання функції, яка приймає покажчик на функцію зворотного виклику учасника, - це аналогія функції покажчиків - також просто:

 C my_c{2}; // aggregate initialization
 int a = 5;
 int b = C_foobar(a, my_c, &C::foo); // call C_foobar with pointer to foo as its callback

3. std::functionоб'єкти (заголовок <functional>)

std::functionКлас є поліморфною функцією обгорткою для зберігання, копіювання або запускайте викликаються об'єкти.

3.1 Написання std::functionпозначень об'єкта / типу

Тип std::functionоб'єкта, що зберігає дзвінок, виглядає так:

std::function<return_type(parameter_type_1, parameter_type_2, parameter_type_3)>

// i.e. using the above function declaration of foo:
std::function<int(int)> stdf_foo = &foo;
// or C::foo:
std::function<int(const C&, int)> stdf_C_foo = &C::foo;

3.2 Позначення зворотного дзвінка

Клас std::functionмає operator()певний , який може бути використаний для виклику своєї мети.

int stdf_foobar (int x, std::function<int(int)> moo)
{
    return x + moo(x); // std::function moo called
}
// or 
int stdf_C_foobar (int x, C const &c, std::function<int(C const &, int)> moo)
{
    return x + moo(c, x); // std::function moo called using c and x
}

3.3 Використання зворотного дзвінка та позначення сумісних типів

Зворотний std::functionвиклик є більш загальним, ніж покажчики функцій або вказівник на функцію члена, оскільки різні типи можуть передаватися та неявно перетворюватися в std::functionоб'єкт.

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

35
@BogeyJammer: Якщо ви цього не помітили: відповідь має дві частини. 1. Загальне пояснення "зворотних викликів" з невеликим прикладом. 2. Вичерпний перелік різних дзвінків і способів написання коду за допомогою зворотних дзвінків. Ви можете не заглиблюватися в деталі і не читати всю відповідь, але тільки тому, що ви не хочете детального перегляду, це не так, що відповідь неефективна або "жорстоко скопійована". Тема "зворотні дзвінки c ++". Навіть якщо частина 1 добре для ОП, інші можуть вважати частину корисною. Сміливо вказуйте на відсутність інформації чи конструктивної критики для першої частини замість -1.
Піксельхімік

1
Частина 1 не є для початківців доброзичливою і зрозумілою. Я не можу бути більш конструктивним, кажучи, що мені не вдалося чогось навчитися. І частина 2 не запитується, затоплюючи сторінку, і не виникає сумнівів, навіть якщо ви робите вигляд, що вона є корисною, незважаючи на те, що вона зазвичай зустрічається у спеціальній документації, де така детальна інформація шукається в першу чергу. Я безумовно зберігаю протилежну заяву. Один голос - це особиста думка, тому прийміть і поважайте її.
Bogey Jammer

24
@BogeyJammer Я не новачок у програмуванні, але я новачок у "сучасному C ++". Ця відповідь дає мені точний контекст, який мені потрібно міркувати про відкликання ролей, зокрема, c ++. ОП може не попросити декількох прикладів, але це звичайно на SO, у нескінченних пошуках виховання світу дурнів, перерахування всіх можливих варіантів вирішення питання. Якщо вона читається занадто багато, як книга, єдиною порадою, яку я можу запропонувати, є попрактикуватись, прочитавши декілька з них .
цві

int b = foobar(a, foo); // call foobar with pointer to foo as callback, це друкарське право? fooповинен бути вказівником для цього для роботи AFAIK.
konoufo

@konoufo: [conv.func]стандарту C ++ 11 говориться: " Значення функції типу T може бути перетворене в первісне значення типу" покажчик на T. " Результатом є вказівник на функцію. "Це стандартне перетворення і як таке відбувається неявно. Тут можна, звичайно, використати покажчик функції.
Піксельхіміст

160

Існує також спосіб C зворотного виклику: функціональні вказівники

//Define a type for the callback signature,
//it is not necessary, but makes life easier

//Function pointer called CallbackType that takes a float
//and returns an int
typedef int (*CallbackType)(float);  


void DoWork(CallbackType callback)
{
  float variable = 0.0f;

  //Do calculations

  //Call the callback with the variable, and retrieve the
  //result
  int result = callback(variable);

  //Do something with the result
}

int SomeCallback(float variable)
{
  int result;

  //Interpret variable

  return result;
}

int main(int argc, char ** argv)
{
  //Pass in SomeCallback to the DoWork
  DoWork(&SomeCallback);
}

Тепер, якщо ви хочете передати методи класу як зворотні дзвінки, декларації до цих функціональних покажчиків мають більш складні декларації, наприклад:

//Declaration:
typedef int (ClassName::*CallbackType)(float);

//This method performs work using an object instance
void DoWorkObject(CallbackType callback)
{
  //Class instance to invoke it through
  ClassName objectInstance;

  //Invocation
  int result = (objectInstance.*callback)(1.0f);
}

//This method performs work using an object pointer
void DoWorkPointer(CallbackType callback)
{
  //Class pointer to invoke it through
  ClassName * pointerInstance;

  //Invocation
  int result = (pointerInstance->*callback)(1.0f);
}

int main(int argc, char ** argv)
{
  //Pass in SomeCallback to the DoWork
  DoWorkObject(&ClassName::Method);
  DoWorkPointer(&ClassName::Method);
}

1
У прикладі методу класу є помилка. Виклик повинен бути: (наприклад. *
Зворотний

Дякую, що вказали на це. Додаю як ілюстрування виклику через об’єкт, так і через вказівник об’єкта.
Рамон Заразуа Б.

3
Це має недолік функції std :: tr1: у тому, що зворотний виклик набирається за класом; це робить недоцільним використання зворотних викликів у стилі С, коли об’єкт, що виконує виклик, не знає клас об'єкта, який потрібно викликати.
відбійник

Як я можу це зробити, не typedefвикористовуючи тип зворотного виклику? Чи можливо це навіть?
Томаш Зато - Відновіть Моніку

1
Так, ти можеш. typedefце просто синтаксичний цукор, щоб зробити його більш читабельним. Без typedefвизначення DoWorkObject для покажчиків на функції буде: void DoWorkObject(int (*callback)(float)). Для вказівників-членів буде:void DoWorkObject(int (ClassName::*callback)(float))
Рамон Заразуа Б.

68

Скотт Мейєрс дає хороший приклад:

class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);

class GameCharacter
{
public:
  typedef std::function<int (const GameCharacter&)> HealthCalcFunc;

  explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
  : healthFunc(hcf)
  { }

  int healthValue() const { return healthFunc(*this); }

private:
  HealthCalcFunc healthFunc;
};

Я думаю, що приклад говорить про це все.

std::function<> це "сучасний" спосіб запису зворотних дзвінків на C ++.


1
У якій книзі SM не цікавить, який приклад подає цей приклад? Ура :)
sam-w

5
Я знаю, що це старе, але оскільки я майже почав це робити, і він не працював над моєю установкою (mingw), якщо ви використовуєте GCC версії <4.x, цей метод не підтримується. Деякі залежності, які я використовую, не складатимуться без великої роботи у версії gcc> = 4.0.1, тому я застряг у використанні старомодних зворотних викликів у стилі С, які працюють чудово.
OzBarry

38

Функція зворотного виклику - це метод, який передається у звичайну процедуру і викликається в якийсь момент рутиною, до якої він передається.

Це дуже корисно для створення програмного забезпечення для багаторазового використання. Наприклад, багато API API операційної системи (наприклад, API Windows) активно використовують зворотні дзвінки.

Наприклад, якщо ви хотіли працювати з файлами в папці - ви можете викликати функцію API за допомогою власного розпорядку, і ваша рутина запускається один раз на файл у вказаній папці. Це дозволяє API бути дуже гнучким.


63
Ця відповідь насправді не означає, що середній програміст нічого не знає. Я вивчаю C ++, будучи знайомим з багатьма іншими мовами. Те, що зворотний дзвінок взагалі, мене не стосується.
Томаш Зато - Відновіть Моніку

17

Прийнята відповідь дуже корисна і досить вичерпна. Однак ОП констатує

Я хотів би побачити простий приклад написання функції зворотного дзвінка.

Отже, ви переходите, від C ++ 11 у вас є, std::functionтому немає необхідності у функціональних покажчиках та подібних матеріалах:

#include <functional>
#include <string>
#include <iostream>

void print_hashes(std::function<int (const std::string&)> hash_calculator) {
    std::string strings_to_hash[] = {"you", "saved", "my", "day"};
    for(auto s : strings_to_hash)
        std::cout << s << ":" << hash_calculator(s) << std::endl;    
}

int main() {
    print_hashes( [](const std::string& str) {   /** lambda expression */
        int result = 0;
        for (int i = 0; i < str.length(); i++)
            result += pow(31, i) * str.at(i);
        return result;
    });
    return 0;
}

Цей приклад до речі якимось реальним, тому що ви хочете викликати функцію print_hashesз різними реалізаціями хеш-функцій, для цього я запропонував просту. Він отримує рядок, повертає int (хеш-значення поданої рядка), і все, що вам потрібно запам’ятати з частини синтаксису, - це те, std::function<int (const std::string&)>що описує таку функцію як вхідний аргумент функції, яка буде викликати її.


З усіх вищезазначених відповідей ця допомогла мені зрозуміти, що таке зворотні дзвінки та як їх використовувати. Дякую.
Mehar

@MeharCharanSahai Радий почути це :) Вас вітають.
Мільєн Мікіч

9

У C ++ немає явного поняття функції зворотного виклику. Механізми зворотного виклику часто реалізовуються за допомогою функціональних покажчиків, об'єктів функтора або об'єктів зворотного виклику. Програмісти повинні чітко розробити та реалізувати функцію зворотного виклику.

Редагувати на основі відгуків:

Незважаючи на негативний відгук, який отримала ця відповідь, це не є помилковим. Я спробую зробити кращу роботу, пояснюючи, звідки я родом.

C і C ++ мають все необхідне для реалізації функцій зворотного дзвінка. Найпоширеніший і тривіальний спосіб реалізації функції зворотного виклику - передача покажчика функції як аргументу функції.

Однак функції зворотного дзвінка та покажчики функцій не є синонімами. Покажчик функції - це мовний механізм, тоді як функція зворотного виклику - це смислове поняття. Покажчики функцій - не єдиний спосіб реалізувати функцію зворотного дзвінка - ви також можете використовувати функтори та навіть віртуальні функції різноманітних садів. Те, що робить функцію зворотного виклику функції, це не механізм, який використовується для ідентифікації та виклику функції, а контекст та семантика виклику. Скажіть, що щось є функцією зворотного виклику, має на увазі більшу, ніж зазвичай, поділ між функцією виклику та конкретною функцією, що викликається, більш слабкою концептуальною зв'язкою між абонентом і позивачем, при цьому абонент має чіткий контроль над тим, що викликається.

Наприклад, документація .NET для IFormatProvider говорить про те, що "GetFormat - метод зворотного виклику" , навіть якщо це лише метод інтерфейсу, який виконується за допомогою заводу. Я не думаю, що хтось заперечуватиме, що всі виклики віртуальних методів є функціями зворотного виклику. Що робить GetFormat методом зворотного виклику не механікою того, як він передається чи викликається, а семантикою того, хто телефонує, що викликає, який метод GetFormat об'єкта буде викликатися.

Деякі мови містять функції з явною семантикою зворотного виклику, як правило, пов'язані з подіями та обробкою подій. Наприклад, C # має тип події із синтаксисом та семантикою, явно розробленими навколо концепції зворотних викликів. Visual Basic має Handles положення, в якому чітко декларує метод є функцією зворотного виклику , в той час як абстрагування від концепції делегатів або покажчиків на функції. У цих випадках смислове поняття зворотного дзвінка інтегрується в саму мову.

З іншого боку, C і C ++ не вкладають семантичну концепцію функцій зворотного виклику майже так само явно. Механізми є, інтегрованої семантики немає. Ви можете реалізувати функції зворотного виклику просто чудово, але щоб отримати щось більш складне, що включає явну семантику зворотного виклику, ви повинні побудувати його над тим, що надає C ++, наприклад, те, що Qt робив зі своїми сигналами та слотами .

У двох словах, у C ++ є те, що вам потрібно для реалізації зворотних викликів, часто досить легко і тривіально, використовуючи функціональні покажчики. У ньому немає ключових слів і функцій, семантика яких є специфічною для зворотних викликів, таких як підвищення , випромінювання , ручки , подія + = і т. Д. Якщо ви переходите з мови з тими елементами, підтримка нативної зворотного дзвінка в C ++ буде відчувати себе кастрированим.


1
на щастя, це була не перша відповідь, яку я прочитав, коли відвідав цю сторінку, інакше я б негайно відбив!
ubugnu

6

Функції зворотного дзвінка є частиною стандарту C, тому також є частиною C ++. Але якщо ви працюєте з C ++, я б запропонував замість цього використовувати шаблон спостерігача : http://en.wikipedia.org/wiki/Observer_pattern


1
Функції зворотного виклику не обов'язково є синонімом виконання функції через покажчик функції, переданий як аргумент. За деякими визначеннями, функція зворотного виклику містить додаткову семантику сповіщення про якийсь інший код про те, що щойно трапилося, або настав час, коли щось має відбутися. З цього погляду функція зворотного виклику не є частиною стандарту C, але може бути легко реалізована за допомогою функціональних покажчиків, які входять до стандарту.
Дарріл

3
"частина стандарту C, отже, також частина C ++." Це типовий нерозуміння, але все-таки непорозуміння :-)
Обмежене спокутування

Я повинен погодитися. Я залишу його таким, яким він є, оскільки він спричинить більше плутанини, якщо я зараз його зміню. Я мав на увазі сказати, що покажчик функції (!) Є частиною стандарту. Казати щось інше від цього - я згоден - вводить в оману.
AudioDroid

Яким чином функції зворотного дзвінка є "частиною стандарту C"? Я не думаю, що той факт, що він підтримує функції та покажчики на функції, означає, що він конкретно канонізує зворотні виклики як мовну концепцію. Крім того, як уже згадувалося, це не було б безпосередньо стосується C ++, навіть якщо це було б точно. І особливо це не актуально, коли ОП запитували "коли і як" використовувати зворотні дзвінки в C ++ (кульгаве, занадто широке запитання, але, тим не менш), а ваша відповідь - це застереження, що стосується лише посилання, замість цього зробити щось інше.
підкреслити_3

4

Див. Вищевикладене визначення, де зазначено, що функція зворотного виклику передається іншій функції, а в якийсь момент вона викликається.

У C ++ бажано мати функції зворотного виклику виклику методу класів. Після цього у вас є доступ до даних учасників. Якщо ви використовуєте спосіб визначення зворотного дзвінка C, вам доведеться вказати його на статичну функцію члена. Це не дуже бажано.

Ось як можна використовувати зворотні дзвінки в C ++. Припустимо 4 файли. Пара файлів .CPP / .H для кожного класу. Клас C1 - це клас із методом, який ми хочемо відкликати. C2 повертається до методу C1. У цьому прикладі функція зворотного виклику приймає 1 параметр, який я додав заради читачів. У прикладі не відображаються будь-які об'єкти, які використовуються при використанні. Один випадок використання для цієї реалізації - коли у вас є один клас, який читає і зберігає дані у тимчасовий простір, а інший, який публікує дані. За допомогою функції зворотного дзвінка для кожного рядка даних, прочитаних, зворотний дзвінок може потім обробляти його. Цей прийом вирізає накладні витрати на необхідний тимчасовий простір. Це особливо корисно для SQL запитів, які повертають велику кількість даних, які потім мають бути оброблені.

/////////////////////////////////////////////////////////////////////
// C1 H file

class C1
{
    public:
    C1() {};
    ~C1() {};
    void CALLBACK F1(int i);
};

/////////////////////////////////////////////////////////////////////
// C1 CPP file

void CALLBACK C1::F1(int i)
{
// Do stuff with C1, its methods and data, and even do stuff with the passed in parameter
}

/////////////////////////////////////////////////////////////////////
// C2 H File

class C1; // Forward declaration

class C2
{
    typedef void (CALLBACK C1::* pfnCallBack)(int i);
public:
    C2() {};
    ~C2() {};

    void Fn(C1 * pThat,pfnCallBack pFn);
};

/////////////////////////////////////////////////////////////////////
// C2 CPP File

void C2::Fn(C1 * pThat,pfnCallBack pFn)
{
    // Call a non-static method in C1
    int i = 1;
    (pThat->*pFn)(i);
}

0

У піднімати торг signals2 дозволяє підписатися загальні функції - члени (без шаблонів!) І в поточно - чином.

Приклад: Сигнали перегляду документа можуть бути використані для реалізації гнучких архітектур перегляду документів. Документ буде містити сигнал, до якого може підключитися кожен із поглядів. Наступний клас Document визначає простий текстовий документ, який підтримує перегляд кількох зображень. Зауважте, що він зберігає єдиний сигнал, до якого будуть підключені всі види.

class Document
{
public:
    typedef boost::signals2::signal<void ()>  signal_t;

public:
    Document()
    {}

    /* Connect a slot to the signal which will be emitted whenever
      text is appended to the document. */
    boost::signals2::connection connect(const signal_t::slot_type &subscriber)
    {
        return m_sig.connect(subscriber);
    }

    void append(const char* s)
    {
        m_text += s;
        m_sig();
    }

    const std::string& getText() const
    {
        return m_text;
    }

private:
    signal_t    m_sig;
    std::string m_text;
};

Далі ми можемо почати визначати погляди. Наступний клас TextView забезпечує простий перегляд тексту документа.

class TextView
{
public:
    TextView(Document& doc): m_document(doc)
    {
        m_connection = m_document.connect(boost::bind(&TextView::refresh, this));
    }

    ~TextView()
    {
        m_connection.disconnect();
    }

    void refresh() const
    {
        std::cout << "TextView: " << m_document.getText() << std::endl;
    }
private:
    Document&               m_document;
    boost::signals2::connection  m_connection;
};

0

Прийнята відповідь є вичерпною, але пов'язаною з питанням, я просто хочу навести простий приклад тут. У мене був код, який я його написав давно. Мені хотілося обрізати дерево порядковим способом (лівий вузол, потім кореневий вузол, потім правий вузол), і коли б я дійшов до одного Вузла, я хотів би мати можливість викликати довільну функцію, щоб це могло робити все.

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

1
як це відповідає на запитання?
Раджан Шарма

ви знаєте, що прийнята відповідь є правильною і вичерпною, і я думаю, що більше немає чого сказати. але я розміщую приклад мого використання функцій зворотного дзвінка.
Ехсан Ахмаді
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.