Примечание. Большинство ответов касаются указателей на функции, что является одной из возможностей достижения логики "обратного вызова" в 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 ++: Callable на 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 Написание указателя функции / обозначения типа
A тип указателя на функцию имеет обозначение
return_type (*)(parameter_type_1, parameter_type_2, parameter_type_3)
// i.e. a pointer to foo has the type:
int (*)(int)
где указатель именованной функции будет выглядеть как
return_type (* name) (parameter_type_1, parameter_type_2, parameter_type_3)
// i.e. f_int_t is a type: function pointer taking one int argument, returning int
typedef int (*f_int_t) (int);
// foo_p is a pointer to function taking int returning int
// initialized by pointer to function foo taking int returning int
int (* foo_p)(int) = &foo;
// can alternatively be written as
f_int_t foo_p = &foo;
Объявление using
дает нам возможность сделать вещи немного более читабельными, поскольку typedef
для f_int_t
также можно записать как:
using f_int_t = int(*)(int);
Где (по крайней мере для меня) яснее, что f_int_t
является псевдонимом нового типа, и распознавание типа указателя на функцию также легче
И объявление функции с использованием обратного вызова типа указателя на функцию будет:
// foobar having a callback argument named moo of type
// pointer to function returning int taking int as its argument
int foobar (int x, int (*moo)(int));
// if f_int is the function pointer typedef from above we can also write foobar as:
int foobar (int x, f_int_t moo);
1.2 Обозначение обратного вызова
Нотация вызова соответствует простому синтаксису вызова функции:
int foobar (int x, int (*moo)(int))
{
return x + moo(x); // function pointer moo called using argument x
}
// analog
int foobar (int x, f_int_t moo)
{
return x + moo(x); // function pointer moo called using argument x
}
1.3 Нотация обратного вызова и совместимые типы
Функция обратного вызова, принимающая указатель на функцию, может быть вызвана с помощью указателей на функцию.
Использование функции, которая принимает обратный вызов указателя функции, довольно просто:
int a = 5;
int b = foobar(a, foo); // call foobar with pointer to foo as callback
// can also be
int b = foobar(a, &foo); // call foobar with pointer to foo as callback
1.4 Пример
Можно написать функцию, которая не зависит от того, как работает обратный вызов:
void tranform_every_int(int * v, unsigned n, int (*fp)(int))
{
for (unsigned i = 0; i < n; ++i)
{
v[i] = fp(v[i]);
}
}
, где возможные обратные вызовы могут быть
int double_int(int x) { return 2*x; }
int square_int(int x) { return x*x; }
используется как
int a[5] = {1, 2, 3, 4, 5};
tranform_every_int(&a[0], 5, double_int);
// now a == {2, 4, 6, 8, 10};
tranform_every_int(&a[0], 5, square_int);
// now a == {4, 16, 36, 64, 100};
2. Указатель на функцию-член
Указатель на функцию-член (некоторого класса C
) - это особый тип (и даже более сложный) указателя на функцию, для работы которого требуется объект типа C
.
struct C
{
int y;
int foo(int x) const { return x+y; }
};
2.1 Запись указателя на функцию-член / нотацию типа
A указатель на тип функции-члена для некоторого класса T
имеет обозначение
// can have more or less parameters
return_type (T::*)(parameter_type_1, parameter_type_2, parameter_type_3)
// i.e. a pointer to C::foo has the type
int (C::*) (int)
где именованный указатель на функцию-член по аналогии с указателем на функцию выглядит следующим образом:
return_type (T::* name) (parameter_type_1, parameter_type_2, parameter_type_3)
// i.e. a type `f_C_int` representing a pointer to member function of `C`
// taking int returning int is:
typedef int (C::* f_C_int_t) (int x);
// The type of C_foo_p is a pointer to member function of C taking int returning int
// Its value is initialized by a pointer to foo of C
int (C::* C_foo_p)(int) = &C::foo;
// which can also be written using the typedef:
f_C_int_t C_foo_p = &C::foo;
Пример: объявление функции, принимающей указатель на обратный вызов функции-члена в качестве одного из аргументов:
// 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;
}