C中的回调函数

新手上路,请多包涵

在 C++ 中,何时以及如何使用回调函数?

编辑:

我想看一个简单的例子来写一个回调函数。

原文由 cpx 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 455
2 个回答

注意:大多数答案都涵盖了函数指针,这是在 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 对象
  • Lambda 表达式
  • 绑定表达式
  • 函数对象(具有重载函数调用运算符的类 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 声明为我们提供了使事情更具可读性的选项,因为 typedeff_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 Lambda 表达式

来自 lambda 表达式的未命名闭包可以存储在 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]);
  }
}

注意:包含的输出打印为模板类型推断的类型名称 Ftype_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 发布,翻译遵循 CC BY-SA 4.0 许可协议

@Pixelchemist 已经给出了全面的答案。但作为一名 Web 开发人员,我可以提供一些提示。

通常我们使用 tcp 来开发 a web framework ,所以通常我们有一个结构:

 TcpServer listen port and register the socket to epoll or something
  -> TcpServer receive new connection
    -> HttpConenction deal the data from the connection
      -> HttpServer call Handler to deal with HttpConnection.
        -> Handler contain codes like save into database and fetch from db

我们可以按照顺序开发框架,但是对于只想关心 Handler 的用户不友好。所以是时候使用 callback 了。

 Mutiple Handler written by user
  -> register the handler as callback property of HttpServer
    -> register the related methods in HttpServer to HttpConnection
      -> register the relate methods in HttpConnection to TcpServer

所以用户只需要将他们的处理程序注册到 httpserver( usually with some path string as key ),另一件事是框架可以做的通用。

所以你会发现我们可以把 callback 作为一种上下文,我们希望委托给其他人为我们做。核心是 we don't know when is the best time to invoke the function, but the guy we delegate to know.

原文由 Bingoabs 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题