【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實現信號槽



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM