程序中经常需要使用函数回调的方式在不同流程中传递信息,以下是几种实现函数回调的方式。
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_cast<mainupdater *>(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_cast<mainupdater *>(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 函数包装实现回调(function+bind方式)
在c++11中可以使用std::function和std::bind将函数直接进行打包,来简化回调的实现。
在子线程类的构造过程中,传入需要被回调的函数,在子线程处理过程中,执行传入的回调函数:
// 子线程类
class mythread : public qthread {
q_object
public:
mythread(std::function<void(int)> in_callback) { // 子线程接收被打包的回调函数
_callback = std::move(in_callback); // 传入回调函数,使用移动语义
}
~mythread(){}
void run() override {
//…… // do something
int result = 9; // 子线程执行完成,记录处理结果
_callback(result); // 调用主流程的回调函数,返回处理结果
}
private:
std::function<void(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::function+std::bind的形式更为简便。但也需要看得懂传统的写法。
到此这篇关于c++中函数回调的几种实现方式的文章就介绍到这了,更多相关c++ 函数回调内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论