一、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::function
。std::function
可以說是函數指針的超集,它除了可以指向全局和靜態函數,還可以指向仿函數,Lambda 表達式,類成員函數,甚至函數簽名不一致的函數,可以說幾乎所有可以調用的對象都可以當做std::function
,當然對於后兩個需要使用std::bind
進行配合。
當然,任何東西都會有優缺點,std::function
填補了函數指針的靈活性,但會對調用性能有一定損耗,經測試發現,在調用次數達 10 億次時,函數指針比直接調用要慢 2 秒左右,而std::function
要比函數指針慢 2 秒左右,這么少的損耗如果是對於調用次數並不高的函數,替換成std::function
絕對是划得來的。
下面我們通過一個例子說明std::function
與std::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::function
與std::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實現信號槽