【C++11 回调函数】function与bind实现函数回调功能(二)

一、std::function与std::bind双剑合璧

因为类成员函数都有一个默认的参数,this,作为第一个参数,这就导致了类成员函数不能直接赋值给std::function,这时候我们就需要std::bind了,简言之,std::bind的作用就是转换函数签名,将缺少的参数补上,将多了的参数去掉,甚至还可以交换原来函数参数的位置。

#include <iostream>
#include <functional>

// 类成员函数
class TestClass
{
public:
	int classMemberFun(int a, int b) { return a + b; }
};

int main() {
	// 类成员函数(使用std::bind绑定类成员函数)
	TestClass testObj;
	std::function<int(int,int)> functional = std::bind(&TestClass::classMemberFun, testObj, std::placeholders::_1, std::placeholders::_2);
	int ret = functional(10, 50);
	std::cout << "类成员函数:" << ret << std::endl;

	return 0;
}

二、std::function与std::bind实现函数回调功能

在 C++11 之前,回调函数一般是通过函数指针实现,函数指针的用法非常简单,但是它只能指向全局或静态函数,这有点太不灵活了,而且我们都知道在 C/C++ 中,全局的东西都很可怕,稍有不慎就会被篡改或随便调用。

但幸好,在 C++11 之后,我们多了一种选择:std::functionstd::function可以说是函数指针的超集,它除了可以指向全局和静态函数,还可以指向仿函数,Lambda 表达式,类成员函数,甚至函数签名不一致的函数,可以说几乎所有可以调用的对象都可以当做std::function,当然对于后两个需要使用std::bind进行配合。

当然,任何东西都会有优缺点,std::function填补了函数指针的灵活性,但会对调用性能有一定损耗,经测试发现,在调用次数达 10 亿次时,函数指针比直接调用要慢 2 秒左右,而std::function要比函数指针慢 2 秒左右,这么少的损耗如果是对于调用次数并不高的函数,替换成std::function绝对是划得来的。


下面我们通过一个例子说明std::functionstd::bind是怎么实现函数回调功能的。

线程类:

#include <iostream>
#include <functional>

using tTask = std::function<void(std::string)>;

// 线程类
class ThreadObject
{
public:
	ThreadObject() {}
	~ThreadObject() {}

public:
	void settask(tTask task)
	{
		m_task = task;
	}
	void run()
	{
		// 回调执行任务函数
		m_task("http://172.0.0.1/test.zip");
	}

private:
	tTask m_task; // std::function类型,调用者,调用回调函数
};

// 下载任务函数,也是回调函数
void downTask(std::string str)
{
	std::cout << "download " << str << std::endl;
}

客户端:

#include "ThreadObject.hpp"
 
// 下载任务函数,也是回调函数
void downTask(std::string str)
{
	std::cout << "download " << str << std::endl;
}

int main() {
	ThreadObject Threadobj;
	Threadobj.settask(std::bind(&downTask, std::placeholders::_1)); // 设置任务函数
	Threadobj.run();

	return 0;
}

三、扩展:std::bind与std::function模拟实现Qt信号槽

Qt 信号槽实现信号的发送和接收,类似观察者。简单说明:

  • sender:发出信号的对象
  • signal:发送对象发出的信号
  • receiver:接收信号的对象
  • slot:接收对象在接收到信号之后所需要调用的函数(槽函数)
  • emit:发送信号

这里准备用std::functionstd::bind模拟实现 Qt 信号槽。


下面实现第一种:

// 信号对象类
class SignalObject
{
public:
	void connect(std::function<void(int)> slotFun)
	{
		m_callFun = slotFun;
	}
	void emitSignal(int signalVal)
	{
		m_callFun(signalVal);
	}

private:
	std::function<void(int)>  m_callFun; // 回调函数,存储槽函数
};

// 槽对象类
class SlotObject
{
public:
	SlotObject() {}
    
public:
	void slotMember(int signalVal)
	{
		std::cout << "signal:" << signalVal << " recv:" << this << std::endl;
	}
};

客户端:

int main() {
	SignalObject signalObject; // 信号对象
	SlotObject slotObject; // 槽对象

	std::cout << "slotObject:" << &slotObject << std::endl;
	// 连接信号槽(此时m_callFun存储着slotMember函数对象)
	signalObject.connect(std::bind(&SlotObject::slotMember, slotObject, std::placeholders::_1));
	// 发射信号
	signalObject.emitSignal(1);

	return 0;
}

输出如下:

slotObject:00D3FDEF
signal:1 recv:00D3FE01

可以发现成功调用了回调函数,并正确接收到了信号,我们的成员函数可以通过回调实现了调用。但是接收者的地址并不是我们定义的 slotobject,即 connect 的是别的对象,具体可以参考开篇链接介绍知,connect 过程发生了拷贝构造。


避免拷贝构造

修改我们的信号类,可以避免拷贝构造:

// 信号对象类2:避免了拷贝构造
class SignalObject2
{
public:
	void connect(SlotObject* recver, std::function<void(SlotObject*, int)> slotFun)
	{
		m_recver = recver; // 保存连接的槽对象
		m_callFun = slotFun;
	}
	void emitSignal(int signal)
	{
		m_callFun(m_recver, signal);
	}
    
private:
	SlotObject* m_recver;
	std::function<void(SlotObject*, int)>  m_callFun;
};

即我们在 connect 时把 recver 保存起来。

客户端:

int main() {
	SignalObject2 signalObject2;
	SlotObject   slotObject;

	std::cout << "slotObject:" << &slotObject << std::endl;
	// 连接信号槽
	std::function<void(SlotObject*, int)> slot = &SlotObject::slotMember;
	signalObject2.connect(&slotObject, slot);
	// 发射信号
	signalObject2.emitSignal(2);

	return 0;
}

输出如下:

slotObject:008FFBD3
signal:2 recv:008FFBD3

sender类实现

当一个槽 slot 和多个信号 signal 连接者,我们并不知道是谁调用的,Qt 中我们知道可以通过 sender() 返回一个 QObject* 来判断,这里模仿实现 sender 方法。

// Object类
class Object
{
public:
	Object* self()
	{
		return this;
	}
	std::function<Object* (void)>  m_sender;
};

// 槽对象类3
class SlotObject3 :public Object
{
public:
	SlotObject3() {}

public:
	void slotMember(int signal)
	{
		if (m_sender) {
			std::cout << "sender:" << m_sender() << std::endl;
		}
		std::cout << "signal:" << signal << " recv:" << this << std::endl;
	}
};

// 信号对象类3
class SignalObject3 :public Object
{
public:
	void connect(SlotObject3* recver, std::function<void(SlotObject3*, int)> slot)
	{
		m_recver = recver;
		m_callFun = slot;
	}
	void emitSignal(int signal)
	{
		m_recver->m_sender = std::bind(&SignalObject3::self, this);
		m_callFun(m_recver, signal);
		m_recver->m_sender = NULL;
	}

private:
	SlotObject3* m_recver;
	std::function<void(SlotObject3*, int)>  m_callFun;
};

即定义一个基类 Object 和一个回调变量 sender,在每次发送时绑定上发送者即可。

客户端:

int main() {
	SignalObject3 signalObject3;
	SlotObject3   slotObject3;

	std::cout << "signalObject3:" << &signalObject3 << std::endl;
	std::cout << "slotObject3:" << &slotObject3 << std::endl;
	// 连接信号槽
	std::function<void(SlotObject3*, int)> slot3 = &SlotObject3::slotMember;
	signalObject3.connect(&slotObject3, slot3);
	// 发射信号
	signalObject3.emitSignal(3);

	return 0;
}

输出如下:

signalObject3:00DDFC40
slotObject3:00DDFC10
sender:00DDFC40
signal:3 recv:00DDFC10

参考:

C++11 std::function 和 std::bind 实现函数回调功能

通过c++11的std::bind及std::function实现类方法回调,模拟Qt实现信号槽


posted @ 2021-03-25 10:03  fengMisaka  阅读(5579)  评论(0编辑  收藏  举报