在 C++ 中,何时以及如何使用回调函数?
编辑:
我想看一个简单的例子来写一个回调函数。
原文由 cpx 发布,翻译遵循 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 许可协议
3 回答2k 阅读✓ 已解决
2 回答3.9k 阅读✓ 已解决
2 回答3.2k 阅读✓ 已解决
1 回答3.2k 阅读✓ 已解决
1 回答2.7k 阅读✓ 已解决
3 回答3.4k 阅读
1 回答1.6k 阅读✓ 已解决
注意:大多数答案都涵盖了函数指针,这是在 C++ 中实现“回调”逻辑的一种可能性,但到目前为止,我认为这还不是最有利的。
什么是回调(?)以及为什么要使用它们(!)
回调是类或函数接受的 _可调用_(见下文),用于根据该回调自定义当前逻辑。
使用回调的一个原因是编写独立于被调用函数中的逻辑并且可以与不同的回调一起使用的 通用 代码。
标准算法库
<algorithm>
的许多函数都使用回调。例如for_each
算法对一系列迭代器中的每个项目应用一元回调:它可用于首先递增然后通过传递适当的可调用对象来打印向量,例如:
哪个打印
回调的另一个应用是通知某些事件的调用者,它可以实现一定程度的静态/编译时间灵活性。
就个人而言,我使用了一个本地优化库,它使用了两个不同的回调:
因此,库设计者不负责决定如何处理通过通知回调提供给程序员的信息,并且他不必担心如何实际确定函数值,因为它们是由逻辑回调提供的。把这些事情做好是图书馆用户的一项任务,并使图书馆保持苗条和更通用。
此外,回调可以启用动态运行时行为。
想象一下某种游戏引擎类,它有一个被触发的函数,每次用户按下键盘上的一个按钮和一组控制你的游戏行为的函数。使用回调,您可以(重新)在运行时决定将采取哪些操作。
这里函数
key_pressed
使用存储在actions
中的回调来获得按下某个键时所需的行为。如果玩家选择改变跳跃按钮,引擎可以调用并因此在下次游戏中按下此按钮时将调用行为更改为
key_pressed
(调用player_jump
)。C++(11) 中的 可调用 对象是什么?
有关更正式的描述,请参阅 C++ 概念:可在 cppreference 上调用。
回调功能可以在 C++(11) 中以多种方式实现,因为有几种不同的东西是 可调用的* :
std::function
对象operator()
)* 注意:指向数据成员的指针也是可调用的,但根本不调用任何函数。
详细写 回调 的几个重要方法
注意:从 C++17 开始,像
f(...)
这样的调用可以写成std::invoke(f, ...)
它还处理指向成员大小写的指针。1.函数指针
函数指针是回调可以具有的“最简单”(就一般性而言;就可读性而言可能是最差的)类型。
让我们有一个简单的函数
foo
:1.1 编写函数指针/类型表示法
函数指针类型 具有符号
命名函数指针 类型看起来像
using
声明为我们提供了使事情更具可读性的选项,因为typedef
的f_int_t
也可以写成:在哪里(至少对我而言)更清楚
f_int_t
是新的类型别名,并且函数指针类型的识别也更容易使用函数指针类型回调的函数 声明将是:
1.2 回调调用符号
调用符号遵循简单的函数调用语法:
1.3 回调使用符号和兼容类型
可以使用函数指针调用带有函数指针的回调函数。
使用带有函数指针回调的函数相当简单:
1.4 示例
可以编写一个不依赖于回调如何工作的函数:
可能的回调可能在哪里
像这样使用
2.指向成员函数的指针
指向成员函数的指针(某些类
C
)是一种特殊类型的(甚至更复杂的)函数指针,它需要类型为C
的对象才能操作。2.1 编写指向成员函数/类型表示法的指针
指向某个类的成员函数类型 的指针
T
具有符号其中一个 指向成员函数的命名指针( 类似于函数指针)如下所示:
示例:声明一个将 指向成员函数回调的指针 作为其参数之一的函数:
2.2 回调调用符号
对于
C
类型的对象,可以通过对取消引用的指针使用成员访问操作来调用指向C
的成员函数的指针。 注意:需要括号!注意:如果指向
C
的指针可用,则语法是等效的(指向C
的指针也必须取消引用):2.3 回调使用符号和兼容类型
回调函数采用类的成员函数指针
T
可以使用类的成员函数指针调用T
。使用带有指向成员函数回调的指针的函数 - 类似于函数指针 - 也非常简单:
3.
std::function
对象(标题<functional>
)std::function
类是一个多态函数包装器,用于存储、复制或调用可调用对象。3.1 编写
std::function
对象/类型表示法std::function
存储可调用对象的类型如下所示:3.2 回调调用符号
类
std::function
operator()
定义了 --- 可用于调用其目标。3.3 回调使用符号和兼容类型
std::function
回调比函数指针或指向成员函数的指针更通用,因为可以传递不同的类型并隐式转换为std::function
对象。3.3.1 函数指针和成员函数指针
函数指针
或指向成员函数的指针
可以使用。
3.3.2 Lambda 表达式
来自 lambda 表达式的未命名闭包可以存储在
std::function
对象中:3.3.3
std::bind
表达式可以传递
std::bind
表达式的结果。例如,通过将参数绑定到函数指针调用:也可以将对象绑定为调用成员函数指针的对象:
3.3.4 函数对象
具有适当
operator()
重载的类的对象也可以存储在std::function
对象中。3.4 示例
更改函数指针示例以使用
std::function
为该函数提供了更多实用程序,因为(参见 3.3)我们有更多使用它的可能性:
4. 模板化回调类型
使用模板,调用回调的代码甚至比使用
std::function
对象更通用。请注意,模板是编译时功能,是编译时多态性的设计工具。如果要通过回调实现运行时动态行为,模板会有所帮助,但它们不会引发运行时动态。
4.1 编写(类型符号)和调用模板化回调
通过使用模板,可以进一步概括上面的
std_ftransform_every_int
代码:回调类型的更通用(也是最简单)的语法是一个普通的、待推导的模板化参数:
注意:包含的输出打印为模板类型推断的类型名称
F
。type_name
的实现在文末给出。范围的一元转换的最通用实现是标准库的一部分,即
std::transform
,它也针对迭代类型进行了模板化。4.2 使用模板化回调和兼容类型的示例
模板化的
std::function
回调方法stdf_transform_every_int_templ
的兼容类型与上述类型相同(见 3.4)。然而,使用模板版本,使用的回调的签名可能会有所改变:
注意:
std_ftransform_every_int
(非模板版本;见上文)适用于foo
但不使用muh
。transform_every_int_templ
的普通模板参数可以是所有可能的可调用类型。上面的代码打印:
type_name
上面使用的实现