【C++】boost::bind和函式物件一起使用實現便捷的非同步程式設計
在C++面向物件程式設計中,觀察者模式是大家熟知的實現非同步程式設計的一種模式。
觀察者模式定義物件間的一種一對多的依賴關係,以便當一個物件的狀態發生改變時,所有依賴於它的物件都得到通知並自動更新。如下圖所示:
觀察者模式提供了一種物件設計,讓主題和觀察者之間鬆耦合:
- 執行時我們用新的觀察者取代現有的觀察者,主題不會受到影響。
- 有新型別的觀察者出現時,主題程式碼不需要更改。
- 可以輕易地獨立使用或複用主題或者觀察者。
- 改變主題或者觀察者的任何一方都不會影響另一方。
UML 關係圖:
傳統的這種觀察者模式雖然是鬆耦合的,但是observer和subject之間還是互相關聯的關係。實現observer的程式碼檔案,必須包含subject的定義標頭檔案;實現subject的程式碼檔案,必須包含observer的定義標頭檔案。由於彼此有稀鬆的互相“包含”關係, 使得觀察者模式的使用得到了很大限制。有的情況下,編譯都難通過,浪費很多時間。這個問題是是典型的“兩個類相互包含的問題”。
問題描述:
A類包含B類的例項,而B類也包含A類的例項
(你可以將A當成subject,而B當成observer)
錯誤的解法
A檔案包含B,而B檔案又包含A檔案,這樣就形成死迴圈 (在本問你中,實現observer的程式碼檔案,必須包含subject的定義標頭檔案;實現subject的程式碼檔案,必須包含observer的定義標頭檔案。subject和observer彼此有稀鬆的互相“包含”關係, 這使得觀察者模式的使用得到了很大限制。)
#include "B.h"
class A
{
int i;
B b;
};
#include "A.h"
class B
{
int i;
A a;
};
l 常規的解決辦法以及注意事項
1)主函式只需要包含b.h 就可以,因為b.h中包含了a.h
2)a.h中不需要包含b.h,但要宣告class b,在避免死迴圈的同時也成功引用了b。
3)包含class b 而沒有包含標頭檔案 "b.h",這樣只能宣告 b型別的指標!!!!而不能例項化!!!!
有沒有更好的辦法呢?當然有。本文就是要解決這個問題。
從理論上將,subject物件是不需要過度關注observer物件的,也就是subject不需要去關聯observer型別。只需要兩步:
(1)observer物件向subject物件發起註冊,可以只傳遞一個函式指標。
(2)當subject有事情需要通知時,可以呼叫上步的函式。
這種方式下,observer 和subject之間的耦合性被進一步降低。subject不再刻意關注observer物件,其組成也不再關聯一個observer物件指標或一個observer物件指標連結串列。
我們用boost::bind這個神兵利器來解決這個問題。
第一步,在observer物件中初始化的時候定義一個boost::function函式物件,如 boost::function<void(NotifyParams)> State_Update_Func;
第二步,將該boost::function物件繫結到本observer物件對接收到subject通知後的處理函式:
State_Update_Func =boost::bind(&Observer::Notification_Process_Func,this,_1);
第三步,進行註冊:
m_Subject.Subscribe ( State_Update_Func);
第四步,實現subject的註冊函式:
inline void Subscribe( boost::function<void(NotifyParams)> Listener) { m_Status_Update_Func=Listener;};
第四步,實現subject的通知函式:
void CSubject::NotifySubscriber()
{
if ( m_Status_Update_Func )
m_Status_Update_Func(m_NotifyParams);
}
這樣,當subject呼叫NotifySubscriber時,observer的處理函式就會被自動呼叫,實現了非同步程式設計。
一個問題討論:
(1)有人認為,通過boost::bind繫結函式物件時,如果被繫結的函式為成員函式,成員函式的引數最好都是傳值,而不要傳遞引用或指標。
例如,假設在多執行緒環境下,我們要根據IP非同步的查詢一個裝置
mp_context->executeInThread(boost::bind(&CMbxdHandler::queryDeviceAsync, this,strIP);
查詢函式最好引數為const string ipaddress,而不是const string &ipaddress
void CQueryHandler::queryDeviceAsync(const string ipaddress)
這樣的理由在於,在多執行緒呼叫環境下,當CQueryHandler被析構時,有可能strIP已經被析構,此時再去使用它的引用,將導致core dump.
其實是多慮了,boost::bind已經幫你考慮了這個問題。boost::bind會自動將每個傳入的引數物件拷貝一份,只有當你顯示將引數設定為boost:ref時,引數才可能是引用傳遞。下面寫一段程式碼來驗證這個問題:
#include "boost/ref.hpp"
#include "boost/function.hpp"
#include "boost/bind.hpp"
#include <string>
#include <iostream>
using namespace std;
class A
{
public:
A(const string& s)
{
_name = s;
}
A(const A& a)
{
_name = a._name;
cout << "copy function" << "\n";
}
void set(const string& s)
{
_name = s;
}
string get() const
{
return _name;
}
private:
string _name;
};
void show_me(const A& a)
{
cout << "show me: " << a.get() << "\n";
}
int main()
{
A* p_a = new A("Jack");
cout << "before bind" << "\n";
boost::function<void ()> f = boost::bind(&show_me, *p_a);
cout << "after bind" << "\n";
p_a->set("Tom");
delete p_a;
f();
f();
f();
}
可以看到,執行結果在bind之前和之後都是內部拷貝的 “Jack”。而不是通過傳遞引用,打印出“tom”