关于C++ 回调函数(callback) 精简且实用

1 关于回调函数

1.1 定义

回调函数的定义,可以很严(复)谨(杂),也可以很简(随)单(意)。其实与其研究定义,还不如讨论为什么需要回调函数,回调函数能干些啥。
在我看来,回调函数不是在函数的定义上区别于普通函数,而是在调用的方式有区别,因为归根到底,他们都是代码中芸芸众生的普普通通的函数,即“回调函数”的重点在“回调”这两个字。
以花钱为例,花钱买衣服叫消费,花钱买股票叫投资,都是花钱,但是方式不同,意义也不同。

下图列举了普通函数执行和回调函数调用的区别。

  • 对于普通函数,就是按照实现设定的逻辑和顺序执行。
  • 对于回调函数,假设Program A和Program B分别有两个人独立开发。回调函数Fun A2它是由Program A定义,但是由Program B调用。Program B只负责取调用Fun A2,但是不管Fun A2函数具体的功能实现。
    image.png

1.2 为什么需要回调函数

因为有这样的使用场景,Fun A2只有在 Fun B1调用时才能执行,有点像中断函数的概念。那可能会有人问,在Program A中不断去查询Fun B1的状态,一旦它被执行了,就让Program A自己去执行Fun A2不行吗?如果你有这样的疑问,说明你已经入门了。
答案是“可以”,但是这样实现的方案不好。因为整个过程中Program A一直都在查询状态,非常耗资源,查询频率高了费CPU,查询频率低了实时性保证不了,Fun B1都执行好久了你才反应过来,Fun A2的执行就会明显晚于Fun B1了。
正因为如此,回调函数才登上了舞台。

如果依然一知半解,可以看这位知乎答主的回答。回调函数(callback)是什么?

2 如何实现函数回调

函数的回调并不复杂,把 Fun A2的函数的地址/指针告诉Program B就可以了。
其实我们在这里要讨论的是在C++中,常用回调函数的类型。
image.png

得到函数的地址是其中一个关键步骤。
普通和函数和类的静态成员函数都分配有确定的函数地址,但是类的普通函数是类共用的,并不会给类的每一个实例都分配一个独立的成员函数,这样就太浪费存储资源了。所以类的非静态函数作为回调函数是有区别的,也是这篇文章想要讨论的重点。

不过我们还以一步一步来,从简单的开始。

2.1 普通函数作为回调函数

#include <iostream>

void programA_FunA1() { printf("I'am ProgramA_FunA1 and be called..\n"); }

void programA_FunA2() { printf("I'am ProgramA_FunA2 and be called..\n"); }

void programB_FunB1(void (*callback)()) {
  printf("I'am programB_FunB1 and be called..\n");
  callback();
}

int main(int argc, char **argv) {
  programA_FunA1();

  programB_FunB1(programA_FunA2);
}

执行结果:
image.png
没有什么可说的,非常简单。

2.2 类的静态函数作为回调函数

#include <iostream>

class ProgramA {
 public:
  void FunA1() { printf("I'am ProgramA.FunA1() and be called..\n"); }

  static void FunA2() { printf("I'am ProgramA.FunA2() and be called..\n"); }
};

class ProgramB {
 public:
  void FunB1(void (*callback)()) {
    printf("I'am ProgramB.FunB1() and be called..\n");
    callback();
  }
};

int main(int argc, char **argv) {
  ProgramA PA;
  PA.FunA1();

  ProgramB PB;
  PB.FunB1(ProgramA::FunA2);
}

执行结果
image.png

可以看出,以上两种方式没有什么本质的区别。
但这种实现有一个很明显的缺点:static 函数不能访问非static 成员变量或函数,会严重限制回调函数可以实现的功能。

2.3 类的非静态函数作为回调函数

#include <iostream>

class ProgramA {
 public:
  void FunA1() { printf("I'am ProgramA.FunA1() and be called..\n"); }

  void FunA2() { printf("I'am ProgramA.FunA2() and be called..\n"); }
};

class ProgramB {
 public:
  void FunB1(void (ProgramA::*callback)(), void *context) {
    printf("I'am ProgramB.FunB1() and be called..\n");
    ((ProgramA *)context->*callback)();
  }
};

int main(int argc, char **argv) {
  ProgramA PA;
  PA.FunA1();

  ProgramB PB;
  PB.FunB1(&ProgramA::FunA2, &PA);  // 此处都要加&
}

执行结果
image.png
这种方法可以得到预期的结果,看似完美,但是也存在明显不足。
比如在programB中FunB1还使用 programA的类型,也就我预先还要知道回调函数所属的类定义,当programB想独立封装时就不好用了。

这里还有一种方法可以避免这样的问题,可以把非static的回调函数 包装为另一个static函数,这种方式也是一种应用比较广的方法。

#include <iostream>

class ProgramA {
 public:
  void FunA1() { printf("I'am ProgramA.FunA1() and be called..\n"); }

  void FunA2() { printf("I'am ProgramA.FunA2() and be called..\n"); }

  static void FunA2Wrapper(void *context) {
    printf("I'am ProgramA.FunA2Wrapper() and be called..\n");
    ((ProgramA *)context)->FunA2();  // 此处调用的FunA2()是context的函数, 不是this->FunA2()
  }
};

class ProgramB {
 public:
  void FunB1(void (ProgramA::*callback)(), void *context) {
    printf("I'am ProgramB.FunB1() and be called..\n");
    ((ProgramA *)context->*callback)();
  }

  void FunB2(void (*callback)(void *), void *context) {
    printf("I'am ProgramB.FunB2() and be called..\n");
    callback(context);
  }
};

int main(int argc, char **argv) {
  ProgramA PA;
  PA.FunA1();

  ProgramB PB;
  PB.FunB1(&ProgramA::FunA2, &PA);  // 此处都要加&

  printf("\n");
  PB.FunB2(ProgramA::FunA2Wrapper, &PA);
}

执行结果:
image.png
这种方法相对于上一种,ProgramB中没有ProgramA的任何信息了,是一种更独立的实现方式。
FunB2()通过调用FunA2Wrapper(),实现间接的对FunA2()的调用。FunA2()可以访问和调用对类内的任何函数和变量。多了一个wrapper函数,也多了一些灵活性。

上面借助wrapper函数实现回调,虽然很灵活,但是还是不够优秀,比如:
1)多了一个不是太有实际用处的wrapper函数。
2)wrapper中还要对传入的指针进行强制转换。
3)FunB2调用时,不但要指定wrapper函数的地址,还要传入PA的地址。

那是否有更灵活、直接的方式呢?有,可以继续往下看。

3 std::funtion和std::bind的使用

std::funtion和std::bind可以登场了。
std::function是一种通用、多态的函数封装。std::function的实例可以对任何可以调用的目标实体进行存储、复制、和调用操作,这些目标实体包括普通函数、Lambda表达式、函数指针、以及其它函数对象等[1]。
std::bind()函数的意义就像它的函数名一样,是用来绑定函数调用的某些参数的[2]。
关于他们的详细用法可以自行百度,如果有需要的以后出一期单独写,这里直接上代码,看std::funtion和std::bind如何在回调中使用。

#include <iostream>

#include <functional> // fucntion/bind

class ProgramA {
 public:
  void FunA1() { printf("I'am ProgramA.FunA1() and be called..\n"); }

  void FunA2() { printf("I'am ProgramA.FunA2() and be called..\n"); }

  static void FunA3() { printf("I'am ProgramA.FunA3() and be called..\n"); }
};

class ProgramB {
  typedef std::function<void ()> CallbackFun;
 public:
   void FunB1(CallbackFun callback) {
    printf("I'am ProgramB.FunB2() and be called..\n");
    callback();
  }
};

void normFun() { printf("I'am normFun() and be called..\n"); }

int main(int argc, char **argv) {
  ProgramA PA;
  PA.FunA1();

  printf("\n");
  ProgramB PB;
  PB.FunB1(normFun);
  printf("\n");
  PB.FunB1(ProgramA::FunA3);
  printf("\n");
  PB.FunB1(std::bind(&ProgramA::FunA2, &PA));
}

执行输出:
image.png
std::funtion支持直接传入函数地址,或者通过std::bind指定。
简而言之,std::funtion是定义函数类型(输入、输出),std::bind是绑定特定的函数(具体的要调用的函数)。

相比于wrapper方法,这个方式要更直接、简洁很多。

Ref:

[1] https://blog.csdn.net/u013654125/article/details/100140328
[2] https://blog.csdn.net/u013654125/article/details/100140547

  • 159
    点赞
  • 448
    收藏
    觉得还不错? 一键收藏
  • 17
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值