C++中的回调函数
在 C++ 中,何时以及如何使用回调函数?
In C++, when and how do you use a callback function?
我想看一个简单的例子来写一个回调函数.
I would like to see a simple example to write a callback function.
推荐答案
注意:大多数答案都涵盖了函数指针,这是在 C++ 中实现回调"逻辑的一种可能性,但到目前为止还不是最有利的我认为.
回调是被类或函数接受的可调用(见下文),用于根据该回调自定义当前逻辑.
A callback is a callable (see further down) accepted by a class or function, used to customize the current logic depending on that callback.
使用回调的一个原因是编写通用代码,该代码独立于被调用函数中的逻辑,并且可以在不同的回调中重用.
One reason to use callbacks is to write generic code which is independant from the logic in the called function and can be reused with different callbacks.
标准算法库的许多函数都使用回调.例如,
for_each
算法对迭代器范围内的每一项应用一元回调:
Many functions of the standard algorithms library <algorithm>
use callbacks. For example the for_each
algorithm applies an unary callback to every item in a range of iterators:
template<class InputIt, class UnaryFunction>
UnaryFunction for_each(InputIt first, InputIt last, UnaryFunction f)
{
for (; first != last; ++first) {
f(*first);
}
return f;
}
可用于先递增,然后通过传递适当的可调用对象来打印向量,例如:
which can be used to first increment and then print a vector by passing appropriate callables for example:
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
回调的另一个应用是通知某些事件的调用者,这可实现一定程度的静态/编译时间灵活性.
Another application of callbacks is the notification of callers of certain events which enables a certain amount of static / compile time flexibility.
就我个人而言,我使用了一个使用两种不同回调的本地优化库:
Personally, I use a local optimization library that uses two different callbacks:
- 如果需要函数值和基于输入值向量的梯度,则调用第一个回调(逻辑回调:函数值确定/梯度推导).
- 第二个回调为每个算法步骤调用一次,并接收有关算法收敛的某些信息(通知回调).
因此,库设计者不负责决定提供给程序员的信息会发生什么通过通知回调,他不必担心如何实际确定函数值,因为它们是由逻辑回调提供的.把这些事情做好是图书馆用户的一项任务,让图书馆保持苗条和更通用.
Thus, the library designer is not in charge of deciding what happens with the information that is given to the programmer via the notification callback and he needn't worry about how to actually determine function values because they're provided by the logic callback. Getting those things right is a task due to the library user and keeps the library slim and more generic.
此外,回调可以启用动态运行时行为.
Furthermore, callbacks can enable dynamic runtime behaviour.
想象一下某种游戏引擎类,它有一个函数,每次用户按下键盘上的一个按钮时都会被触发,并有一组控制你的游戏行为的函数.通过回调,您可以(重新)在运行时决定将采取哪些操作.
Imagine some kind of game engine class which has a function that is fired, each time the users presses a button on his keyboard and a set of functions that control your game behaviour. With callbacks you can (re)decide at runtime which action will be taken.
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
中的回调来在按下某个键时获得所需的行为.如果玩家选择改变跳跃按钮,引擎可以调用
Here the function key_pressed
uses the callbacks stored in actions
to obtain the desired behaviour when a certain key is pressed.
If the player chooses to change the button for jumping, the engine can call
game_core_instance.update_keybind(newly_selected_key, &player_jump);
从而在下次游戏中按下此按钮时更改对key_pressed
(调用player_jump
)的调用行为.
and thus change the behaviour of a call to key_pressed
(which the calls player_jump
) once this button is pressed the next time ingame.
请参阅 cppreference 上的 C++ 概念:可调用以获得更正式的描述.
See C++ concepts: Callable on cppreference for a more formal description.
回调功能可以在 C++(11) 中以多种方式实现,因为有几种不同的东西是可调用的*:
Callback functionality can be realized in several ways in C++(11) since several different things turn out to be callable*:
- 函数指针(包括指向成员函数的指针)
std::function
对象- Lambda 表达式
- 绑定表达式
- 函数对象(具有重载函数调用运算符
operator()
的类)
- Function pointers (including pointers to member functions)
std::function
objects- Lambda expressions
- Bind expressions
- Function objects (classes with overloaded function call operator
operator()
)
* 注意:指向数据成员的指针也是可调用的,但根本不调用任何函数.
- X.1 在这篇文章中编写"一个回调是指声明和命名回调类型的语法.
- X.2调用"回调是指调用这些对象的语法.
- X.3使用"回调是指使用回调将参数传递给函数时的语法.
注意:从 C++17 开始,像 f(...)
这样的调用可以写成 std::invoke(f, ...)
也处理指向成员大小写的指针.
Note: As of C++17, a call like f(...)
can be written as std::invoke(f, ...)
which also handles the pointer to member case.
函数指针是回调可以具有的最简单"(就通用性而言;就可读性而言可能是最差的)类型.
A function pointer is the 'simplest' (in terms of generality; in terms of readability arguably the worst) type a callback can have.
让我们有一个简单的函数foo
:
Let's have a simple function 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
声明让我们可以选择使事情更易读,因为 f_int_t
的 typedef
也可以写成:
The using
declaration gives us the option to make things a little bit more readable, since the typedef
for f_int_t
can also be written as:
using f_int_t = int(*)(int);
哪里(至少对我来说)更清楚的是f_int_t
是新的类型别名,函数指针类型的识别也更容易
Where (at least for me) it is clearer that f_int_t
is the new type alias and recognition of the function pointer type is also easier
使用函数指针类型回调的函数声明将是:
And a declaration of a function using a callback of function pointer type will be:
// 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 回调调用符号
调用符号遵循简单的函数调用语法:
1.2 Callback call notation
The call notation follows the simple function call syntax:
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 回调使用符号和兼容类型
可以使用函数指针调用带有函数指针的回调函数.
1.3 Callback use notation and compatible types
A callback function taking a function pointer can be called using function pointers.
使用接受函数指针回调的函数相当简单:
Using a function that takes a function pointer callback is rather simple:
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 示例
可以编写一个不依赖于回调如何工作的函数:
1.4 Example
A function ca be written that doesn't rely on how the callback works:
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
类型的对象来操作
2. Pointer to member function
A pointer to member function (of some class C
) is a special type of (and even more complex) function pointer which requires an object of type C
to operate on.
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)
其中成员函数的命名指针 - 类似于函数指针 - 看起来像这样:
where a named pointer to member function will -in analogy to the function pointer- look like this:
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;
示例:声明一个将指向成员函数回调的指针作为其参数之一的函数:
Example: Declaring a function taking a pointer to member function callback as one of its arguments:
// 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
类型的对象.注意:需要括号!
2.2 Callback call notation
The pointer to member function of C
can be invoked, with respect to an object of type C
by using member access operations on the dereferenced pointer.
Note: Parenthesis required!
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
的指针也必须取消引用):
Note: If a pointer to C
is available the syntax is equivalent (where the pointer to C
must be dereferenced as well):
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
类成员函数指针的回调函数.
2.3 Callback use notation and compatible types
A callback function taking a member function pointer of class T
can be called using a member function pointer of class T
.
使用一个接受成员函数回调指针的函数 - 类似于函数指针 - 也非常简单:
Using a function that takes a pointer to member function callback is -in analogy to function pointers- quite simple as well:
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
对象(头文件
)
std::function
类是一个多态函数包装器,用于存储、复制或调用可调用对象.
3. std::function
objects (header <functional>
)
The std::function
class is a polymorphic function wrapper to store, copy or invoke callables.
存储可调用对象的 std::function
对象的类型如下所示:
The type of a std::function
object storing a callable looks like:
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()
,可用于调用其目标.
3.2 Callback call notation
The class std::function
has operator()
defined which can be used to invoke its target.
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 Callback use notation and compatible types
The std::function
callback is more generic than function pointers or pointer to member function since different types can be passed and implicitly converted into a std::function
object.
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 Lambda 表达式
来自 lambda 表达式的未命名闭包可以存储在 std::function
对象中:
An unnamed closure from a lambda expression can be stored in a std::function
object:
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
表达式
3.3.3 std::bind
expressions
std::bind
表达式的结果可以被传递.例如通过将参数绑定到函数指针调用:
The result of a std::bind
expression can be passed. For example by binding parameters to a function pointer call:
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 )
还可以绑定对象作为调用成员函数指针的对象:
Where also objects can be bound as the object for the invocation of pointer to member functions:
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
对象中.
Objects of classes having a proper operator()
overload can be stored inside a std::function
object, as well.
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)我们有更多的可能性来使用它:
gives a whole lot more utility to that function because (see 3.3) we have more possibilities to use it:
// 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
对象更通用.
请注意,模板是编译时功能,是编译时多态性的设计工具.如果要通过回调实现运行时动态行为,模板会有所帮助,但不会引起运行时动态.
泛化,即上面的 std_ftransform_every_int
代码可以通过使用模板进一步实现:
Generalizing i.e. the std_ftransform_every_int
code from above even further can be achieved by using templates:
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]);
}
}
回调类型的语法更通用(也是最简单的),它是一个简单的、待推导出的模板化参数:
with an even more general (as well as easiest) syntax for a callback type being a plain, to-be-deduced templated argument:
template<class F>
void transform_every_int_templ(int * v,
unsigned const n, F f)
{
std::cout << "transform_every_int_templ<"
<< type_name<F>() << ">
";
for (unsigned i = 0; i < n; ++i)
{
v[i] = f(v[i]);
}
}
注意:包含的输出打印为模板化类型 F
推导出的类型名称.type_name
的实现在本文末尾给出.
Note: The included output prints the type name deduced for templated type F
. The implementation of type_name
is given at the end of this post.
范围的一元变换最通用的实现是标准库的一部分,即std::transform
,这也针对迭代类型进行了模板化.
The most general implementation for the unary transformation of a range is part of the standard library, namely std::transform
,
which is also templated with respect to the iterated types.
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).
4.2 Examples using templated callbacks and compatible types
The compatible types for the templated std::function
callback method stdf_transform_every_int_templ
are identical to the above mentioned types (see 3.4).
然而,使用模板版本,所用回调的签名可能会发生一些变化:
Using the templated version however, the signature of the used callback may change a little:
// 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
.
Note: std_ftransform_every_int
(non templated version; see above) does work with foo
but not using 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 << "
";
}
transform_every_int_templ
的普通模板参数可以是所有可能的可调用类型.
The plain templated parameter of transform_every_int_templ
can be every possible callable type.
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
上面使用的实现
type_name
implementation used above
#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;
}
相关文章