• 【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实现信号槽


  • 相关阅读:
    结对第一次—疫情统计可视化(原型设计)
    寒假作业(2/2)
    寒假作业(1/2)
    android 全局悬浮窗的使用以及权限
    个人作业——软件工程实践总结&个人技术博客
    个人作业——软件评测
    结对第二次作业——某次疫情统计可视化的实现
    软工实践寒假作业(2/2)
    软工实践寒假作业(1/2)
    蒟蒻博主又回来了
  • 原文地址:https://www.cnblogs.com/linuxAndMcu/p/14576162.html
Copyright © 2020-2023  润新知