C++/6/C++23下回调函数(callback) 的n种用法
最后编辑时间2024-09-18 18:05:13 星期三
C++23下回调函数(callback) 的n种用法
前置知识:
- C++
 - 现代C++模板
 - C
 
使用环境
- VS 2022 下 C++23标准
 
回调函数干嘛用的
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
来看个小场景:
假设你买了食材food,然后要进行处理,写个代码:
//你的做饭方法
void Fun1(std::string food){...}
void Fun2(std::string food){...}
......
void FunN(std::string food){...}
//实际使用的函数
void handle(std::string food,std::string method)
{
	if(method=="炒")
	{
		Fun1(food);
	}
	else if(method=="煮")
	{
		Fun2(food);
	}
	......
	else
	{
		FunN(food);
	}
}
//使用
handle("萝卜","炒");
这么干有以下几个缺点:
- 难以维护,每次新增做菜方法就要重新写一次
hadle() - 缺少自由性,
hadle()一定要位于方法下方,否则报错 - 实际上
hadle()中绝大部分代码是if,else用来判断要用哪个方法,是无效代码 
我们想:
是否可以将函数像变量一样传入进来,不必在意表示,然后统一使用?类似这样:
//实际使用的函数
void handle(std::string food,auto methodFun)
{
	methodFun(food);
}
//你的做饭方法
void Fun1(std::string food){...}
//使用
handle("萝卜",Fun1);
这不就优雅多了吗,此时的methodFun()函数,就被称为回调函数;
先读完我再往下看
函数 和 函数指针
在这里先插播一个问题:
这个会打印出什么
int A(){std::cout<<"A";}
std::cout<<A<<std::endl;
//注意是A,不是A()
此时打印出来的是A的地址,在打印的时候,函数A发生了隐式转换,从函数隐式转化为了函数指针,这里可以看看mq白cpp大佬的b站视频,请务必先了解 函数 和 函数指针 的差别和联系再往下看
请把编译器开到C++20版本以上
具体搜索引擎搜索以下关键字
vs2022 C++23
gcc C++23
VS Code C++23
C 中的回调函数
要理解C++中的回调函数,先看C中的回调函数是怎么干的
#include <iostream>
//被测试函数1
int test(int data)
{
	std::cout << "触发test函数,data="<<data << std::endl;
	return data;
}
//使用函数指针
void Fun1(int (*Fun0)(int), int data)
{
	std::cout << "触发Fun1函数," << std::endl;
	Fun0(data);
}
int main()
{
	Fun(test,2);  //注意这里test不带括号!!!
	return 0;
}
//输出:
//触发Fun1函数
//触发test函数,data=2
此处int (*Fun0)(int)就是函数指针的形式参数(简称‘形参’),如果你要传入的函数有两个int参数,则应该使用int (*Fun0)(int,int);Fun0是形参名字,类似后面的data,可以改成你喜欢的名字例如atri;(* )不可省略!(后面会讲省略的情况),其它的自行类比即可。
比较简单,套公式即可,C的写法C++也可以使用。
使用int Fun0(int)作为函数形参
在刚刚的例子中,套一个(*  )就是为了表示这是个函数,但是Fun0()后面的括号已经告诉了这是个函数了,那我们就可以偷懒一下
#include <iostream>
//被测试函数1
int test(int data)
{
	std::cout << "触发test函数,data="<<data << std::endl;
	return data;
}
//使用int Fun0(int)作为函数形参
void Fun1(int Fun0(int), int data)
{
	std::cout << "触发Fun1函数," << std::endl;
	Fun0(data);
}
int main()
{
	Fun(test,2);  //注意这里test不带括号!!!
	return 0;
}
//输出:
//触发Fun1函数
//触发test函数,data=2
舒服又符合直觉对吧,这个特性官网也没说,笔者测试在vs2022,gcc 均能过测,同时与C++版本无关,推测为int (*Fun0)(int)的语法糖,十分甚至九分的推荐。
使用auto Fun作为函数形参
这种写法有违C/C++语言设计精神,请合理使用
笔者在写php的时候,想到:C++能不能直接把参数当作函数调用(借助auto)呢?欸,试试看
#include <iostream>
//被测试函数1
int test(int data)
{
	std::cout << "触发test函数,data="<<data << std::endl;
	return data;
}
//使用auto Fun作为函数形参
//请注意,此时这里不是普通函数了!!!
void Fun1(auto Fun0, int data)
{
	std::cout << "触发Fun1函数," << std::endl;
	Fun0(data);
}
int main()
{
	Fun(test,2);  //注意这里test不带括号!!!
	return 0;
}
//输出:
//触发Fun1函数
//触发test函数,data=2
没问题!
但是
这里的Fun1 可不是普通的函数,它是模板的函数,是封装过的函数,它此时是一个C++20引入的简写模板
笔者能力不足,水平有限,还在学习约束和模板,在这里就不展开讲,给有能力的各位一些关键字来方便搜索
- 模板
 - 函数模板
 - 简写模板
 - 约束
 
对类方法进行回调
类内也有方法,那可不可以对类内方法进行回调呢?
关于->* ::*可以看mq白cpp大佬的视频:这里
class classA
{
public:
	//被测试函数3,静态函数
	static int staticTestClassA(int data)
	{
		std::cout << "触发staticTestClassA函数" << "data = " << data << std::endl;
		return data;
	}
	//被测试函数4
	int testClassA(int data)
	{
		std::cout << "触发testClassA函数" << "data = " << data << std::endl;
		return data;
	}
};
class classB
{
public:
	//类的非静态函数作为回调函数
	int testClassB(classA* a,int(classA::* Fun0)(int),int data)
	{
		std::cout << "触发testClassB函数" << "data = " << data << std::endl;
		((classA*)a->*Fun0)(data);
		return data;
	}
};
//使用函数指针
void Fun1(int (*Fun0)(int), int data)
{
	std::cout << "触发Fun1函数," << std::endl;
	Fun0(data);
}
int main()
{
	//调用类静态方法
	Fun1(classA::staticTestClassA,1);
	//调用动态方法
	classA a;
	classB b;
	b.testClassB(&a, &classA::testClassA,1);
}
- 类内静态方法
可以看到,和普通的函数没什么区别,但是要带上类名:: - 类方法
类方法不会隐式转换为函数指针,所以要使用&classA::testClassA,同时,使用类的非静态函数作为回调函数的写法是固定的 
int testClassB(classA* a,int(classA::* Fun0)(int),int data) //正确
int testClassB(classA* a,int classA::Fun0(int),int data)//错误
int testClassB(classA a,int(classA::* Fun0)(int),int data)//错误
如果想尝试auto的话,也可以,但是涉及类操作不推荐用auto,会出现一系列推导问题
欸,写Qt的小伙伴可能发现,这不就是槽函数吗?,是的,Qt就是基于回调函数来实现槽函数的
使用std::bind进行回调
包含文件
#include<functional>
std::bind的头文件是<functional>它是一个函数适配器,接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。
参考文章:点我
模板原型
//绑定普通函数
template< class F, class... Args >
/*unspecified*/ bind( F&& f, Args&&... args );
//绑定成员函数
template< class R, class F, class... Args >
/*unspecified*/ bind( F&& f, Args&&... args );
绑定普通函数
//被测试函数1
int test(int data)
{
	std::cout << "触发test函数,data="<<data << std::endl;
	return data;
}
auto a = std::bind(test,std::placeholders::_1);
//使用
std::cout<<a(10)<<std::endl;
其中,test是被调用的函数名,std::placeholders::_1是占位用的,可以直接指定,例如:
int test(int data,int data2)
{
	return data+data2;
}
auto a = std::bind(test,20,std::placeholders::_2);
//使用
std::cout<<a(10)<<std::endl;
看到了咩,直接封装好了,像普通函数一样使用就好了。
绑定成员函数
classA a;
auto b = std::bind(&classA::testClassA, &a, std::placeholders::_1);
//使用
std::cout<<b(10)<<std::endl;
- bind绑定类成员函数时,第一个参数表示对象的成员函数的指针,第二个参数表示对象的地址。
 - 必须显式地指定&classA::testClassA,因为编译器不会将对象的成员函数隐式转换成函数指针,所以必须在classA::testClassA前添加&;(类方法不会隐式转化为函数指针)
 - 使用对象成员函数的指针时,必须要知道该指针属于哪个对象,因此第二个参数为对象的地址 &base;
 
使用std::function进行回调
包含文件
#include<functional>
类模板 std::function 是通用多态函数封装器。 std::function 的实例能存储、复制及调用任何可调用 (Callable) 目标——函数、 lambda 表达式、 bind 表达式或其他函数对象,还有指向成员函数指针和指向数据成员指针。
存储的可调用对象被称为 std::function 的目标。若 std::function 不含目标,则称它为空。调用空 std::function 的目标导致抛出 std::bad_function_call 异常。
std::function 满足可复制构造 (CopyConstructible) 和可复制赋值 (CopyAssignable) 。
参考文章:点我
模板原型:
template< class R, class... Args >
class function<R(Args...)>;
使用方法:
                 //↓被调用函数的参数列表
std::function<int(int)> a = (被调用函数名);
           //  ↑被调用函数的返回值
绑定普通函数
//被测试函数1
int test(int data)
{
	std::cout << "触发test函数,data="<<data << std::endl;
	return data;
}
std::function<int(int)> a= text ;
//使用
std::cout<<a(10)<<std::endl;
其中,test是被调用的函数名
看到了咩,直接封装好了,像普通函数一样使用就好了。
绑定成员函数(需要配合std::bind使用)
classA a;
std::function<int(int)> b = std::bind(&classA::testClassA, &a, std::placeholders::_1);
//使用
std::cout<<b(10)<<std::endl;
不是哥么,这不是一样的吗?为什么还要多一个std::function呢?,来,看看std::bind的类型我相信你会理解的:

当你要写一个函数的时候
int c(std::function<int(int)>){}
你试试看用std::bind写个看看
总结
回调函数比较常见的用途在于,在多线程中,一个线程不需要也无法每时每刻检测其它线程运行情况,只要当条件准备充足时候,直接调用预先存储好的回调函数,就可以通知其它线程的对象达到线程间通讯的效果。(在Qt中槽函数的原理就是这个)
例如:
//控制用
class A
{
public:
	void run()//主要运行函数
	{
		B b;
		b->ok = std::bind(&A::isOK, this);
		......//多线程操作,将b换到其他线程,a就可以处理另外的事情了
		b->add();//在多线程中
	}
	void isOK(int data)//后续操作
	{
		std::cout<<data<<std::endl;//进行操作
	}
}
//打工人
class B
{
public:
	void add()//耗时操作
	{
		......
		this->ok(data);//发出信号,传出结果
	}
	std::function<void(int)> ok;//存储控制的回调函数
}
//使用
A a;
a->run();
                    
                
                
            
        
浙公网安备 33010602011771号