C++/6/C++23下回调函数(callback) 的n种用法

最后编辑时间2024-09-18 18:05:13 星期三

C++23下回调函数(callback) 的n种用法

前置知识:

  1. C++
  2. 现代C++模板
  3. C

使用环境

  1. 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("萝卜","炒");

这么干有以下几个缺点:

  1. 难以维护,每次新增做菜方法就要重新写一次hadle()
  2. 缺少自由性,hadle()一定要位于方法下方,否则报错
  3. 实际上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引入的简写模板
笔者能力不足,水平有限,还在学习约束和模板,在这里就不展开讲,给有能力的各位一些关键字来方便搜索

  1. 模板
  2. 函数模板
  3. 简写模板
  4. 约束

对类方法进行回调

类内也有方法,那可不可以对类内方法进行回调呢?
关于->* ::*可以看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);
}

  1. 类内静态方法
    可以看到,和普通的函数没什么区别,但是要带上类名::
  2. 类方法
    类方法不会隐式转换为函数指针,所以要使用&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的类型我相信你会理解的:
image

当你要写一个函数的时候

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();

posted @ 2024-09-18 23:10  归海言诺  阅读(166)  评论(0)    收藏  举报