C++中函数回调的几种实现方式
文章目录函数回调的几种实现方式1 c风格函数指针实现回调callback方式使用函数指针实现回调在类内使用函数指针实现回调在类内直接使用函数指针会出现的问题采用包装函数 void * 进行改进额外逻辑风险子线程不能直接操作 Qt UI 控件2 抽象接口实现回调sink方式3 函数包装实现回调functionbind方式总结函数回调的几种实现方式程序中经常需要使用函数回调的方式在不同流程中传递信息以下是几种实现函数回调的方式。1 c风格函数指针实现回调callback方式callback方式本质上是直接将函数指针传递给执行者。使用函数指针实现回调示例主流程需要子流程完成某个任务任务结束后通过回调函数通知主流程。子流程定义接口用于接收回调函数并接收回调函数及参数在指定时机调用回调函数。// 声明回调函数指针类型 // 参数一结果记录 typedef void(* Callback)(int); // 子流程执行函数 void doWork(Callback in_callback) { ... int result 9; // 子流程执行完成记录处理结果 in_callback(result); // 执行结束调用主流程的回调函数返回处理结果 }主流程定义回调函数调用子流程并传入回调函数。// 回调函数属于主流程被子流程调用 void globalCallback(int result) { // result 就是子流程传过来的结果 std::cout 主流程回调收到子流程结果 result std::endl; } // 主流程执行函数 int main() { doWork(globalCallback); // 调用子流程传入结果变量 return 0; }在类内使用函数指针实现回调常规的函数指针只能保存全局函数或静态函数无法指向类的成员函数因为类的成员函数的首个参数是隐含的this指针。如果子流程是以面向对象的方式编写写在了独立的类中则需要做额外的包装。在类内直接使用函数指针会出现的问题示例主线程类需要子线程类完成某个任务任务结束后通过回调函数通知主线程类。如果依然使用函数指针指向类的成员函数的方式会导致编译问题。子流程 MyThread 类// 声明回调函数指针类型 // 参数一结果记录 // 参数二上下文包含类对象的this指针 typedef void(* Callback)(int); // 子线程类 class MyThread : public QThread{ Q_OBJECT public: MyThread(Callback in_callback) { // 传入回调函数及参数 _callback in_callback; } virtual void run() { //…… // do something int result 9; // 子线程执行完成记录处理结果 _callback(result); // 执行结束调用主流程的回调函数返回处理结果 } private: Callback _callback nullptr; // 在子线程中声明回调函数指针 };主线程 MainUpdater 类主线程是否是独立类并无影响只提供调用场所// 主线程UI类 class MainUpdater { public: MainUpdater() { //创建子线程并传入回调函数 //_thread new MyThread(MainUpdater::workFinished, int _result); // 此处编译报错 } void workFinished(int result){ // 需要被执行回调 // result 就是子流程传过来的结果 std::cout 主流程回调收到子流程结果 result std::endl; } private: MyThread * _thread nullptr; int _result 0; };上述示例中在创建子线程并传入回调函数的过程中会报错因为Callback是void(*)(int, void *)类型的函数指针而传入的workFinished成员函数的首个参数是隐含的this指针导致类型无法匹配。采用包装函数 void * 进行改进使用额外的包装函数能够解决上述问题添加全局或者静态的包装函数配合void *上下文参数可以将this指针传入子线程。把类对象指针塞到void * context传给子流程回调函数使用全局函数或者静态成员函数额外包装一层匹配函数指针在静态包装函数内部把void * context强转回类对象指针手动调用成员函数手动传入 this。// 声明回调函数指针类型 // 参数一结果记录 // 参数二上下文包含类对象的this指针 typedef void(* Callback)(int, void * context); // 子线程类 class MyThread : public QThread{ Q_OBJECT public: MyThread(Callback in_callback, void * context) { // 传入回调函数及参数 _callback in_callback; _context context; } void run() override { //…… // do something int result 9; // 子线程执行完成记录处理结果 _callback(result, _context); // 执行结束调用主流程的回调函数返回处理结果 } private: Callback _callback nullptr; // 在子线程中声明回调函数指针 void * _context nullptr; // 在子线程中接收回调函数的额外参数 };主线程 MainUpdater 类主线程是否是独立类并无影响只提供调用场所// 使用全局函数将回调函数包装一层 void callback_wapper(int result, void * context) { MainUpdater * main_obj static_castMainUpdater *(context); main_obj-workFinished(result); // 在包装函数中调用回调函数 } // 主线程UI类 class MainUpdater { public: MainUpdater() { //创建子线程并传入回调函数 _thread new MyThread(callback_wapper, this); // 传入包装函数和 this 指针 _thread-start(); } void workFinished(int result) { // 需要被执行回调 // result 就是子流程传过来的结果 std::cout 主流程回调收到子流程结果 result std::endl; } private: MyThread * _thread; };上述例子中使用了全局函数做为包装函数也可以使用MainUpdater类的静态成员函数做为包装函数因为这两种函数都不会隐含this指针做为首个函数参数可以被常规函数指针绑定。额外逻辑风险子线程不能直接操作 Qt UI 控件如果代码底层是 QT 框架workFinished()运行在子线程不能直接操作 Qt 主线程的 UI 控件。可以使用invokeMethod配合lambda表达式将任务投递到主线程执行void callback_wapper(int result, void * context) { MainUpdater * main_obj static_castMainUpdater *(context); // 将任务投递到主线程参数一所在的线程消息队列 QMetaObject::invokeMethod(main_obj, [](){ // lambda表达式 // 此处运行在主线程可以安全操作QWidget控件 std::cout invokeMethod主线程 result std::endl; // 可以安全操作 UI 控件 // ui-label-setText(...); }, Qt::QueuedConnection); // 跨线程需要使用队列连接 }invokeMethod利用 Qt 元对象系统支持跨线程调用函数把任务投递到首个参数QObject绑定的线程的事件循环中执行。使用对象和目标对象首个参数都必须继承自QObject并且开启元对象系统。2 抽象接口实现回调sink方式不再使用函数指针和裸指针强转而是定义一个抽象接口让调用者实现。sink翻译成汇或者槽是表示接收事件、接收结果的一方类似把数据 “接过来消化掉”的意思。// 子线程类 class MyThread : public QThread { Q_OBJECT public: explicit MyThread(IWorkSink* sink) // 直接传入 sink 接口指针 : m_sink(sink) { } protected: void run() override { //…… // do something int result 9; // 子线程执行完成记录处理结果 m_sink-onWorkFinished(result);// 调用主流程的回调函数返回处理结果 } private: IWorkSink* m_sink nullptr; };// sink 抽象接口回调接收器 class IWorkSink { public: virtual ~IWorkSink() default; // 虚析构为了子类的顺利析构 virtual void onWorkFinished(int result) 0; // 回调通知接口子线程完成后调用 }; // 主线程 UI 类 class MainUpdater : public QWidget, public IWorkSink{ // 主线程类继承 Sink 抽象接口 Q_OBJECT public: MainUpdater() { //创建子线程 _thread new MyThread(this); // 把 this 传给子线程实现了 IWorkSink 接口 _thread-start(); } void onWorkFinished(int result) override { // 主线程实现 sink 中的纯虚函数接口 std::cout sink收到子线程结果 result std::endl; // 如果要操作QWidget UI必须投递回主线程 QMetaObject::invokeMethod(this, [](){ // ui-label-setText(QString::number(result)); }, Qt::QueuedConnection); } private: MyThread * _thread nullptr; };注意接口回调方法依旧运行在子线程操作 UI 仍然需要QMetaObject::invokeMethod切回主线程。3 函数包装实现回调functionbind方式在c11中可以使用std::function和std::bind将函数直接进行打包来简化回调的实现。在子线程类的构造过程中传入需要被回调的函数在子线程处理过程中执行传入的回调函数// 子线程类 class MyThread : public QThread { Q_OBJECT public: MyThread(std::functionvoid(int) in_callback) { // 子线程接收被打包的回调函数 _callback std::move(in_callback); // 传入回调函数使用移动语义 } ~MyThread(){} void run() override { //…… // do something int result 9; // 子线程执行完成记录处理结果 _callback(result); // 调用主流程的回调函数返回处理结果 } private: std::functionvoid(int) _callback; // 在子线程中声明打包的回调函数 };在主线程类中创建子线程的时候传入需要进行回调的实际函数// UI类 class MainUpdater : public QWidget { Q_OBJECT public: MainUpdater(QWidget * parent 0) { // 创建子线程 // 传入的参数是使用 bind 打包好的回调函数参数一是回调函数地址参数二是隐含的this指针参数三是回调函数的参数 _thread new MyThread(std::bind(MainUpdater::workFinished, this, std::placeholders::1)); } ~MainUpdater(); void workFinished(int result) { // 需要被执行回调 std::cout 收到子线程结果 result std::endl; // 如果要操作QWidget UI必须投递回主线程 QMetaObject::invokeMethod(this, [](){ // ui-label-setText(QString::number(result)); }, Qt::QueuedConnection); } private: MyThread *_thread; };总结现代编程实现回调函数一般使用std::functionstd::bind的形式更为简便。但也需要看得懂传统的写法。