Функции обратного вызова в C ++

303

В C ++, когда и как вы используете функцию обратного вызова?

РЕДАКТИРОВАТЬ:
Я хотел бы увидеть простой пример, чтобы написать функцию обратного вызова.

мп
источник
[This] ( 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 ++ concept: Callable на cppreference для более формального описания.

Функциональность обратного вызова может быть реализована несколькими способами в C ++ (11), поскольку несколько разных вещей могут быть вызваны * :

  • Указатели на функции (включая указатели на функции-члены)
  • std::function объекты
  • Лямбда-выражения
  • Привязка выражений
  • Объекты функций (классы с перегруженным оператором вызова функций operator())

* Примечание: указатель на элементы данных также может быть вызван, но функция не вызывается вообще.

Несколько важных способов написать обратные вызовы в деталях

  • X.1 «Написание» обратного вызова в этом посте означает синтаксис для объявления и присвоения имени типу обратного вызова.
  • X.2 «Вызов» обратного вызова относится к синтаксису для вызова этих объектов.
  • X.3 «Использование» обратного вызова означает синтаксис при передаче аргументов функции с использованием обратного вызова.

Примечание. Начиная с C ++ 17, f(...)можно записать вызов like, 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;
}
Pixelchemist
источник
35
@ BogeyJammer: Если вы не заметили: ответ состоит из двух частей. 1. Общее объяснение «обратных вызовов» с небольшим примером. 2. Полный список различных вызываемых объектов и способов написания кода с использованием обратных вызовов. Вы можете не копаться в деталях и не читать весь ответ, а просто потому, что вам не нужен детальный просмотр, это не тот случай, когда ответ неэффективен или «зверски скопирован». Тема "Обратные вызовы c ++". Даже если часть 1 подходит для OP, другие могут найти часть 2 полезной. Не стесняйтесь указывать на отсутствие информации или конструктивную критику по первой части вместо -1.
Pixelchemist
1
Часть 1 не новичок и достаточно понятен. Я не могу быть более конструктивным, говоря, что мне не удалось чему-то научиться. И часть 2, которая не была запрошена, заполняет страницу, и о ней не может быть и речи, даже если вы делаете вид, что она полезна, несмотря на то, что ее обычно можно найти в специальной документации, где такую ​​подробную информацию ищут в первую очередь. Я определенно держу пониженную оценку. Один голос представляет личное мнение, поэтому, пожалуйста, примите и уважайте его.
Bogey Jammer
24
@ BogeyJammer Я не новичок в программировании, но я новичок в "современном C ++". Этот ответ дает мне точный контекст, который мне нужен для размышления о роли, которую играют обратные вызовы, в частности, в c ++. У ОП, возможно, не было просьб о множестве примеров, но в СЦ, в бесконечном стремлении обучить мир дураков, перечислить все возможные решения вопроса. Если она читается слишком похоже на книгу, единственный совет, который я могу предложить, - немного попрактиковаться, прочитав несколько из них .
декабря
int b = foobar(a, foo); // call foobar with pointer to foo as callback, это опечатка не так ли? fooдолжен быть указатель, чтобы это работало AFAIK.
Конуфо
@konoufo: [conv.func]стандарта C ++ 11 гласит: « Значение l типа функции T может быть преобразовано в значение типа« указатель на T ». Результатом является указатель на функцию ". Это стандартное преобразование и, как таковое, происходит неявно. Можно (конечно) использовать здесь указатель на функцию.
Pixelchemist
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
В примере метода класса есть ошибка. Вызов должен быть: (экземпляр. * Обратный вызов) (1.0f)
КарлДжонсон
Спасибо, что указали на это. Я добавлю как для иллюстрации вызова через объект, так и через указатель объекта.
Рамон Заразуа Б.
3
Это имеет недостаток от std :: tr1: function в том, что обратный вызов набирается для каждого класса; это делает нецелесообразным использование обратных вызовов в стиле C, когда объект, выполняющий вызов, не знает класс вызываемого объекта.
Блефер
Как я могу сделать это без 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
Из какого интереса в какой книге СМ дает этот пример? Приветствия :)
Sam-W
5
Я знаю, что это старая версия, но так как я почти начал это делать, и она перестала работать на моей установке (mingw), если вы используете GCC версии <4.x, этот метод не поддерживается. Некоторые из зависимостей, которые я использую, не будут компилироваться без большой работы в версии gcc> = 4.0.1, поэтому я застрял с использованием хороших старомодных обратных вызовов в стиле C, которые прекрасно работают.
Озбарри
38

Функция обратного вызова - это метод, который передается в подпрограмму и в какой-то момент вызывается подпрограммой, которой он передается.

Это очень полезно для создания программного обеспечения многократного использования. Например, многие 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&)>это описание такой функции как входной аргумент функции, которая ее вызовет.

Мильен Микич
источник
Из всех приведенных выше ответов этот заставил меня понять, что такое обратные вызовы и как их использовать. Спасибо.
Мехар Чаран Сахай
@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

AudioDroid
источник
1
Функции обратного вызова не обязательно являются синонимами выполнения функции через указатель функции, который был передан в качестве аргумента. По некоторым определениям термин функция обратного вызова несет дополнительную семантику уведомления некоторого другого кода о том, что только что произошло, или пора что-то случиться. С этой точки зрения функция обратного вызова не является частью стандарта C, но может быть легко реализована с помощью указателей функций, которые являются частью стандарта.
Дэррил
3
"часть стандарта C, следовательно, также часть C ++." Это типичное недоразумение, но, тем не менее, недоразумение :-)
Ограниченное искупление
Я должен согласиться. Я оставлю это, как есть, так как это только вызовет больше беспорядка, если я изменю это сейчас. Я хотел сказать, что указатель на функцию (!) Является частью стандарта. Сказать что-то отличное от этого - я согласен - вводит в заблуждение.
AudioDroid
Каким образом функции обратного вызова являются «частью стандарта C»? Я не думаю, что тот факт, что он поддерживает функции и указатели на функции, означает, что он специально канонизирует обратные вызовы как концепцию языка. Кроме того, как уже упоминалось, это не будет иметь непосредственного отношения к C ++, даже если оно будет точным. И это особенно не актуально, когда ОП спрашивает «когда и как» использовать обратные вызовы в C ++ (хромой, слишком широкий вопрос, но тем не менее), и ваш ответ - это только ссылка, содержащая указание сделать что-то другое.
underscore_d
4

См. Определение выше, где говорится, что функция обратного вызова передается какой-то другой функции, и в какой-то момент она вызывается.

В C ++ желательно, чтобы функции обратного вызова вызывали метод классов. Когда вы делаете это, у вас есть доступ к данным участника. Если вы используете способ определения обратного вызова на языке C, вам придется указать на статическую функцию-член. Это не очень желательно.

Вот как вы можете использовать обратные вызовы в C ++. Предположим, 4 файла. Пара файлов .CPP / .H для каждого класса. Класс C1 - это класс с методом, который мы хотим вызвать. С2 обращается к методу С1. В этом примере функция обратного вызова принимает 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;
};
crizCraig
источник
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
как это отвечает на вопрос?
Раджан Шарма
Вы знаете, что принятый ответ является правильным и исчерпывающим, и я думаю, что больше нечего сказать вообще. но я публикую пример моего использования функций обратного вызова.
Эхсан Ахмади