轉: 深刻理解信號槽機制

https://blog.csdn.net/liuuze5/article/details/53523463html

 

深刻理解信號槽(一)ios

 

這篇文章來自於 A Deeper Look at Signals and Slots,Scott Collins 2005.12.19。須要說明的是,咱們這裏所說的「信號槽」不只僅是指 Qt 庫裏面的信號槽,而是站在一個全局的高度,從系統的角度來理解信號槽。因此在這篇文章中,Qt 信號槽僅僅做爲一種實現來介紹,咱們還將介紹另一種信號槽的實現——boost::signal。所以,當你在文章中看到一些信號的名字時,或許僅僅是爲了描述方便而杜撰的,實際並無這個信號。程序員

什麼是信號槽?編程

這個問題咱們能夠從兩個角度來回答,一個簡短一些,另一個則長些。安全

讓咱們先用最簡潔的語言來回答這個問題——什麼是信號槽?架構

  • 信號槽是觀察者模式的一種實現,或者說是一種昇華;
  • 一個信號就是一個可以被觀察的事件,或者至少是事件已經發生的一種通知;
  • 一個槽就是一個觀察者,一般就是在被觀察的對象發生改變的時候——也能夠說是信號發出的時候——被調用的函數;
  • 你能夠將信號和槽鏈接起來,造成一種觀察者-被觀察者的關係;
  • 當事件或者狀態發生改變的時候,信號就會被髮出;同時,信號發出者有義務調用全部註冊的對這個事件(信號)感興趣的函數(槽)。

信號和槽是多對多的關係。一個信號能夠鏈接多個槽,而一個槽也能夠監聽多個信號。app

信號能夠有附加信息。例如,窗口關閉的時候可能發出 windowClosing 信號,而這個信號就能夠包含着窗口的句柄,用來代表到底是哪一個窗口發出這個信號;一個滑塊在滑動時可能發出一個信號,而這個信號包含滑塊的具體位置,或者新的值等等。咱們能夠把信號槽理解成函數簽名。信號只能同具備相同簽名的槽鏈接起來。你能夠把信號當作是底層事件的一個形象的名字。好比這個 windowClosing 信號,咱們就知道這是窗口關閉事件發生時會發出的。框架

信號槽實際是與語言無關的,有不少方法均可以實現信號槽,不一樣的實現機制會致使信號槽差異很大。信號槽這一術語最初來自 Trolltech 公司的 Qt 庫(如今已經被 Nokia 收購)。1994年,Qt 的第一個版本發佈,爲咱們帶來了信號槽的概念。這一律念馬上引發計算機科學界的注意,提出了多種不一樣的實現。現在,信號槽依然是 Qt 庫的核心之一,其餘許多庫也提供了相似的實現,甚至出現了一些專門提供這一機制的工具庫。ide

簡單瞭解信號槽以後,咱們再來從另一個角度回答這個問題:什麼是信號槽?它們從何而來?函數

前面咱們已經瞭解了信號槽相關的概念。下面咱們將從更細緻的角度來探討,信號槽機制是怎樣一步步發展的,以及怎樣在你本身的代碼中使用它們。

程序設計中很重要的一部分是組件交互:系統的一部分須要告訴另外一部分去完成一些操做。讓咱們從一個簡單的例子開始:

// C++
class Button
{
public:
    void clicked(); // something that happens: Buttons may be clicked
};
class Page
{
public:
    void reload(); // ...which I might want to do when a Button is clicked
};

 

 

換句話說,Page 類知道如何從新載入頁面(reload),Button 有一個動做是點擊(click)。假設咱們有一個函數返回當前頁面 currentPage(),那麼,當 button 被點擊的時候,當前頁面應該被從新載入。

// C++ --- making the connection directly
void Button::clicked()
{
    currentPage()->reload(); // Buttons know exactly what to do when clicked
}

 

這看起來並不很好。由於 Button 這個類名彷佛暗示了這是一個可重用的類,可是這個類的點擊操做卻同 Page 牢牢地耦合在一塊兒了。這使得只要 button 一被點擊,一定調用 currentPage() 的 reload() 函數。這根本不能被重用,或許把它更名叫 PageReloadButton 更好一些。

實際上,不得不說,這確實是一種實現方式。若是 Button::click() 這個函數是 virtual 的,那麼你徹底能夠寫一個新類去繼承這個 Button:

// C++ --- connecting to different actions by specializing
class Button
{
public:
    virtual void clicked() = 0; // Buttons have no idea what to do when clicked
};

class PageReloadButton : public Button
{
public:
    virtual void clicked() {
        currentPage()->reload(); // ...specialize Button to connect it to a specific action
    }
};

 

好了,如今 Button 能夠被重用了。可是這並非一個很好的解決方案。

引入回調

讓咱們停下來,回想一下在只有 C 的時代,咱們該如何解決這個問題。若是隻有 C,就不存在 virtual 這種東西。重用有不少種方式,可是因爲沒有了類的幫助,咱們採用另外的解決方案:函數指針。

/* C --- connecting to different actions via function pointers */
void reloadPage_action( void* ) /* one possible action when a Button is clicked */
{
    reloadPage(currentPage());
}

void loadPage_action( void* url ) /* another possible action when a Button is clicked */
{
    loadPage(currentPage(), (char*)url);
}

struct Button {
    /* ...now I keep a (changeable) pointer to the function to be called */
    void (*actionFunc_)();
    void* actionFuncData_;
};

void buttonClicked( Button* button )
{
    /* call the attached function, whatever it might be */
    if ( button && button->actionFunc_ )
        (*button->actionFunc_)(button->actionFuncData_);
}

 

這就是一般所說的「回調」。buttonClicked() 函數在編譯期並不知道要調用哪個函數。被調用的函數是在運行期傳進來的。這樣,咱們的 Button 就能夠被重用了,由於咱們能夠在運行時將不一樣的函數指針傳遞進來,從而得到不一樣的點擊操做。

增長類型安全

對於 C++ 或者 Java 程序員來講,老是不喜歡這麼作。由於這不是類型安全的(注意 url 有一步強制類型轉換)。

咱們爲何須要類型安全呢?一個對象的類型其實暗示了你將如何使用這個對象。有了明確的對象類型,你就可讓編譯器幫助你檢查你的代碼是否是被正確的使用了,如同你畫了一個邊界,告訴編譯器說,若是有人越界,就要報錯。然而,若是沒有類型安全,你就丟失了這種優點,編譯器也就不能幫助你完成這種維護。這就如同你開車同樣。只要你的速度足夠,你就可讓你的汽車飛起來,可是,通常來講,這種速度就會提醒你,這太不安全了。同時還會有一些裝置,好比雷達之類,也會時時幫你檢查這種狀況。這就如同編譯器幫咱們作的那樣,是咱們出浴一種安全使用的範圍內。

回過來再看看咱們的代碼。使用 C 不是類型安全的,可是使用 C++,咱們能夠把回調的函數指針和數據放在一個類裏面,從而得到類型安全的優點。例如:

// re-usable actions, C++ style (callback objects)
class AbstractAction
{
public:
    virtual void execute() = 0; // sub-classes re-implement this to actually do something
};

class Button
{
    // ...now I keep a (changeable) pointer to the action to be executed
    AbstractAction* action_;
};

void Button::clicked()
{
    // execute the attached action, whatever it may be
    if ( action_ )
        action_->execute();
}

class PageReloadAction : public AbstractAction
    // one possible action when a Button is clicked
{
public:
    virtual void execute() {
        currentPage()->reload();
    }
};
class PageLoadAction : public AbstractAction
    // another possible action when a Button is clicked
{
public:
    // ...
    virtual void execute() {
        currentPage()->load(url_);
    }
private:
    std::string url_;
};

 

好了!咱們的 Button 已經能夠很方便的重用了,而且也是類型安全的,再也沒有了強制類型轉換。這種實現已經能夠解決系統中遇到的絕大部分問題了。彷佛如今的解決方案同前面的相似,都是繼承了一個類。只不過如今咱們對動做進行了抽象,而以前是對 Button 進行的抽象。這很像前面 C 的實現,咱們將不一樣的動做和 Button 關聯起來。如今,咱們一步步找到一種比較使人滿意的方法。

 

 深刻理解信號槽(二) 

 

多對多

下一個問題是,咱們可以在點擊一次從新載入按鈕以後作多個操做嗎?也就是讓信號和槽實現多對多的關係?

實際上,咱們只須要利用一個普通的鏈表,就能夠輕鬆實現這個功能了。好比,以下的實現:

class MultiAction : public AbstractAction
    // ...an action that is composed of zero or more other actions;
    // executing it is really executing each of the sub-actions
{
public:
    // ...
    virtual void execute();
private:
    std::vector<AbstractAction*> actionList_;
    // ...or any reasonable collection machinery
};

void MultiAction::execute()
{
    // call execute() on each action in actionList_
    std::for_each( actionList_.begin(),
                   actionList_.end(),
                   boost::bind(&AbstractAction::execute, _1) );
}

 

 

這就是其中的一種實現。不要以爲這種實現看上去沒什麼水平,實際上咱們發現這就是一種至關簡潔的方法。同時,不要糾結於咱們代碼中的 std:: 和 boost:: 這些命名空間,你徹底能夠用另外的類,強調一下,這只是一種可能的實現。如今,咱們的一個動做能夠鏈接多個 button 了,固然,也能夠是別的 Action 的使用者。如今,咱們有了一個多對多的機制。經過將 AbstractAction* 替換成 boost::shared_ptr,能夠解決 AbstractAction 的歸屬問題,同時保持原有的多對多的關係。

這會有不少的類!

若是你在實際項目中使用上面的機制,不少就會發現,咱們必須爲每個 action 定義一個類,這將不可避免地引發類爆炸。至今爲止,咱們前面所說的全部實現都存在這個問題。不過,咱們以後將着重討論這個問題,如今先不要糾結在這裏啦!

特化!特化!

當咱們開始工做的時候,咱們經過將每個 button 賦予不一樣的 action,實現 Button 類的重用。這實際是一種特化。然而,咱們的問題是,action 的特化被放在了固定的類層次中,在這裏就是這些 Button 類。這意味着,咱們的 action 很難被更大規模的重用,由於每個 action 實際都是與 Button 類綁定的。那麼,咱們換個思路,能不能將這種特化放到信號與槽鏈接的時候進行呢?這樣,action 和 button 這二者都沒必要進行特化了。

函數對象

將一個類的函數進行必定曾度的封裝,這個思想至關有用。實際上,咱們的 Action 類的存在,就是將 execute() 這個函數進行封裝,其餘別無用處。這在 C++ 裏面仍是比較廣泛的,不少時候咱們用 ++ 的特性從新封裝函數,讓類的行爲看起來就像函數同樣。例如,咱們重載 operator() 運算符,就可讓類看起來很像一個函數:

class AbstractAction
{
public:
    virtual void operator()() = 0;
};

// using an action (given AbstractAction& action)
action();

 

這樣,咱們的類看起來很像函數。前面代碼中的 for_each 也得作相應的改變:

// previously
std::for_each( actionList_.begin(),
               actionList_.end(),
               boost::bind(&AbstractAction::execute, _1) );
// now
std::for_each( actionList_.begin(),
               actionList_.end(),
               boost::bind(&AbstractAction::operator(), _1) );

 

如今,咱們的 Button::clicked() 函數的實現有了更多的選擇:

// previously
action_->execute();

// option 1: use the dereferenced pointer like a function
(*action_)();

// option 2: call the function by its new name
action_->operator()();

 

看起來很麻煩,值得這樣作嗎?

下面咱們來試着解釋一下信號和槽的目的。看上去,重寫 operator() 運算符有些過度,並不值得咱們去這麼作。可是,要知道,在某些問題上,你提供的可用的解決方案越多,越有利於咱們編寫更簡潔的代碼。經過對一些類進行規範,就像咱們要讓函數對象看起來更像函數,咱們可讓它們在某些環境下更適合重用。在使用模板編程,或者是 Boost.Function,bind 或者是模板元編程的情形下,這一點尤其重要。

這是對無需更多特化創建信號槽鏈接重要性的部分回答。模板就提供了這樣一種機制,讓添加了特化參數的代碼並不那麼難地被特化,正如咱們的函數對象那樣。而模板的特化對於使用者而言是透明的。

鬆耦合

如今,讓咱們回顧一下咱們以前的種種作法。

咱們執着地尋求一種可以在同一個地方調用不一樣函數的方法,這其實是 C++ 內置的功能之一,經過 virtual 關鍵字,固然,咱們也可使用函數指針實現。當咱們須要調用的函數沒有一個合適的簽名,咱們將它包裝成一個類。咱們已經演示瞭如何在同一地方調用多個函數,至少咱們知道有這麼一種方法(但這並非在編譯期完成的)。咱們實現了讓「信號發送」可以被若干個不一樣的「槽」監聽。

不過,咱們的系統的確沒有什麼很是不同凡響的地方。咱們來仔細審覈一下咱們的系統,它真正不一樣的是:

  • 定義了兩個不一樣的術語:「信號」和「槽」;
  • 在一個調用點(信號)與零個或者多個回調(槽)相連;
  • 鏈接的焦點從提供者處移開,更多地轉向消費者(也就是說,Button 並不須要知道如何作是正確的,而是由回調函數去告知 Button,你須要調用我)。

可是,這樣的系統還遠達不到鬆耦合的關係。Button 類並不須要知道 Page 類。鬆耦合意味着更少的依賴;依賴越少,組件的可重用性也就越高。

固然,確定須要有組件同時知道 Button 和 Page,從而完成對它們的鏈接。如今,咱們的鏈接實際是用代碼描述的,若是咱們不用代碼,而用數據描述鏈接呢?這麼一來,咱們就有了鬆耦合的類,從而提升兩者的可重用性。

新的鏈接模式

什麼樣的鏈接模式纔算是非代碼描述呢?假如僅僅只有一種信號槽的簽名,例如 void (*signature)(),這並不能實現。使用散列表,將信號的名字映射到匹配的鏈接函數,將槽的名字映射到匹配的函數指針,這樣的一對字符串便可創建一個鏈接。

然而,這種實現其實包含一些「握手」協議。咱們的確但願具備多種信號槽的簽名。在信號槽的簡短回答中咱們提到,信號能夠攜帶附加信息。這要求信號具備參數。咱們並無處理成員函數與非成員函數的不一樣,這又是一種潛在的函數簽名的不一樣。咱們尚未決定,咱們是直接將信號鏈接到槽函數上,仍是鏈接到一個包裝器上。若是是包裝器,這個包裝器須要已經存在呢,仍是咱們在須要時自動建立呢?雖然底層思想很簡單,可是,真正的實現還須要很好的努力才行。彷佛經過類名可以建立對象是一種不錯的想法,這取決於你的實現方式,有時候甚至取決於你有沒有能力作出這種實現。將信號和槽放入散列表須要一種註冊機制。一旦有了這麼一種系統,前面所說的「有太多類了」的問題就得以解決了。你所須要作的就是維護這個散列表的鍵值,而且在須要的時候實例化類。

給信號槽添加這樣的能力將比咱們前面所作的全部工做都困可貴多。在由鍵值進行鏈接時,多數實現都會選擇放棄編譯期類型安全檢查,以知足信號和槽的兼容。這樣的系統代價更高,可是其應用也遠遠高於自動信號槽鏈接。這樣的系統容許實例化外部的類,好比 Button 以及它的鏈接。因此,這樣的系統有很強大的能力,它可以完成一個類的裝配、鏈接,並最終完成實例化操做,好比直接從資源描述文件中導出的一個對話框。既然它可以憑藉名字使函數可用,這就是一種腳本能力。若是你須要上面所說的種種特性,那麼,完成這麼一套系統絕對是值得的,你的信號槽系統也會從中受益,由數據去完成信號槽的鏈接。

對於不須要這種能力的實現則會忽略這部分特性。從這點看,這種實現就是「輕量級」的。對於一個須要這些特性的庫而言,完整地實現出來就是一個輕量級實現。這也是區別這些實現的方法之一。

 

深刻理解信號槽(三) 

 

信號槽的實現實例—— Qt 和 Boost

Qt 的信號槽和 Boost.Signals 因爲有着大相徑庭的設計目標,所以兩者的實現、強度也十分不一樣。將兩者混合在一塊兒使用也不是不可能的,咱們將在本系統的最後一部分來討論這個問題。

使用信號槽

信號槽是偉大的工具,可是如何能更好的使用它們?相比於直接函數調用,有三點值得咱們的注意。一個信號槽的調用:

  • 或許會比直接函數調用耗費更多的時間/空間;
  • 可能不能使用 inline;
  • 對於代碼閱讀者來講可能並不友好。

使用信號槽進行解耦,咱們得到的最大的好處是,鏈接兩端的對象不須要知道對方的任何信息。Button 同動做的鏈接是一個很典型的案例。例如以下信息:

class Elevator
{
public:
    enum Direction { DownDirection=-1, NoDirection=0, UpDirection=1 };
    enum State { IdleState, LoadingState, MovingState };
    // ...
// signals:
    void floorChanged( int newFloor );
    void stateChanged( State newState );
    void directionChanged( Direction newDirection );
};

 

 

Elevator 類,也就是電梯,不須要知道有多少顯示器正在監聽它的信號,也不須要知道這些顯示器的任何信息。每一層可能有一個屏幕和一組燈,用於顯示電梯的當前位置和方向,另一些遠程操控的面板也會顯示出一樣的信息。電梯並不關心這些東西。當它穿過(或者停在)某一層的時候,它會發出一個 floorChanged(int) 信號。或許,交通訊號燈是更合適的一個例子。

你也能夠實現一個應用程序,其中每個函數調用都是經過信號來觸發的。這在技術上說是徹底沒有問題的,然而倒是不大可行的,由於信號槽的使用無疑會喪失一部分代碼可讀性和系統性能。如何在這其中作出平衡,也是你須要考慮的很重要的一點。

Qt 方式

瞭解 Qt 信號槽最好的莫過於 Qt 的文檔。不過,這裏咱們從一個小例子來了解信號槽的 Qt 方式的使用。

// Qt Signals and Slots

class Button : public QObject
{
    Q_OBJECT
Q_SIGNALS:
    void clicked();
};

class Page : public QObject
{
    Q_OBJECT
public Q_SLOTS:
    void reload();
};

// given pointers to an actual Button and Page:
connect(button, SIGNAL(clicked()), page, SLOT(reload()));

 

Boost.Signals 方式

瞭解 Boost.Signals 的最好方式一樣是 Boost 的文檔。這裏,咱們仍是先從代碼的角度瞭解一下它的使用。

// Boost.Signals

class Button
{
public:
    boost::signal< void() > clicked;
};

class Page
{
public:
    void reload();
};

// given pointers to an actual Button and Page:
button->clicked.connect( boost::bind(&Page::reload, page) );

 

對比

或許你已經注意到上面的例子中,不管是 Qt 的實現方式仍是 Boost 的實現方式,除了必須的 Button 和 Page 兩個類以外,都不須要額外的類。兩種實現都解決了類爆炸的問題。下面讓咱們對照着來看一下咱們前面的分析。如今咱們有:

  • 兩個不一樣的術語以及各自的動做:信號和槽;
  • 在一個地方(信號)能夠鏈接零個或者多個回調函數(槽),同時也是多對多的;
  • 焦點在於鏈接自己,而不是提供者或者消費者;
  • 不須要手工爲了一個鏈接建立新的類;
  • 鏈接仍舊是類型安全的。
這五點是信號槽系統的核心,Qt 和 boost 都擁有這些特性。下面則是兩者的不一樣之處:
 Boost.Signals  Qt Signals 和 Slots
 一個信號就是一個對象  信號只能是成員函數
發出信號相似於函數調用  發出信號相似於函數調用,Qt 提供了一個 emit 關鍵字來完成這個操做
信號能夠是全局的、局部的或者是成員對象  信號只能是成員函數
 任何可以訪問到信號對象的代碼均可以發出信號  只有信號的擁有者才能發出信號
 槽是任何可被調用的函數或者函數對象  槽是通過特別設計的成員函數
 能夠有返回值,返回值能夠在多個槽中使用  沒有返回值
 同步的  同步的或者隊列的
 非線程安全  線程安全,能夠跨線程使用
 當且僅當槽是可追蹤的時候,槽被銷燬時,鏈接自動斷開  槽被銷燬時,鏈接都會自動斷開(由於全部槽都是可追蹤的)
 類型安全(編譯器檢查)  類型安全(運行期檢查)
 參數列表必須徹底一致  槽能夠忽略信號中多餘的參數
 信號、槽能夠是模板  信號、槽不能是模板
 C++ 直接實現

  經過由 moc 生成的元對象實現(moc 以及元對象系統都是 C++ 直接實現的)

  沒有內省機制  能夠經過內省發現

 能夠經過元對象調用

 鏈接能夠從資源文件中自動推斷出

最重要的是,Qt 的信號槽機制已經深深地植入到框架之中,成爲不可分割的一部分。它們可使用 Qt 專門的開發工具,例如 QtCreator,經過拖拽的方式很輕鬆的建立、刪除、修改。它們甚至能夠經過動態加載資源文件,由特定命名的對象自動動態生成。這些都是 boost 做爲一個通用庫所不可能提供的。

 

深刻理解信號槽(四) 

 

 

將 Qt 的信號槽系統與 Boost.Signals 結合使用

實際上,將 Qt 的信號槽系統與 Boost.Signals 結合在一塊兒使用並不是不可能。經過前面的闡述,咱們都知道了兩者的不一樣,至於爲何要將這兩者結合使用,則是見仁見智的了。這裏,咱們給出一種結合使用的解決方案,可是並非說咱們暗示應該將它們結合使用。這應該是具體問題具體分析的。

將 Qt 的信號槽系統與 Boost.Signals 結合使用,最大的障礙是,Qt 使用預處理器定義了關鍵字 signals,slots 以及 emit。這些能夠看作是 Qt 對 C++ 語言的擴展。同時,Qt 也提供了另一種方式,即便用宏來實現這些關鍵字。爲了屏蔽掉這些擴展的關鍵字,Qt 4.1 的 pro 文件引入了 no_keywords 選項,以便使用標準 C++ 的方式,方便 Qt 與其餘 C++ 同時使用。你能夠經過打開 no_keywords 選項,來屏蔽掉這些關鍵字。下面是一個簡單的實現:

 

# TestSignals.pro (platform independent project file, input to qmake)
# showing how to mix Qt Signals and Slots with Boost.Signals
  #
  # Things you will have in your .pro when you try this...
  #
CONFIG += no_keywords
  # so Qt won't #define any non-all-caps `keywords'
INCLUDEPATH += . /usr/local/include/boost-1_33_1/
  # so we can #include <boost/someheader.hpp>
macx:LIBS += /usr/local/lib/libboost_signals-1_33_1.a
  # ...and we need to link with the Boost.Signals library.
  # This is where it lives on my Mac,
  # other platforms would have to add a line here
  #
  # Things specific to my demo
  #
CONFIG -= app_bundle
  # so I will build a command-line tool instead of a Mac OS X app bundle
HEADERS += Sender.h Receiver.h
SOURCES += Receiver.cpp main.cpp

 

請注意,咱們已經在 pro 文件中打開了 no_keywords 選項,那麼,相似 signals 這樣的關鍵字已經不起做用了。因此,咱們必須將這些關鍵字修改爲相應的宏的版本。例如,咱們須要將 signals 改成 Q_SIGNALS,將 slots 改成 Q_SLOTS 等等。請看下面的代碼:

// Sender.h

#include <QObject>
#include <string>
#include <boost/signal.hpp>
class Sender : public QObject
{
    Q_OBJECT
Q_SIGNALS: // a Qt signal
    void qtSignal( const std::string& );
    // connect with
    // QObject::connect(sender, SIGNAL(qtSignal(const std::string&)), ...
public: // a Boost signal for the same signature
    boost::signal< void ( const std::string& ) > boostSignal;
    // connect with
    // sender->boostSignal.connect(...
public: // an interface to make Sender emit its signals
    void sendBoostSignal( const std::string& message ) {
        boostSignal(message);
    }

    void sendQtSignal( const std::string& message ) {
        qtSignal(message);
    }
};

 

如今咱們有了一個發送者,下面來看看接收者:

// Receiver.h
#include <QObject>
#include <string>

class Receiver : public QObject
{
    Q_OBJECT
public Q_SLOTS:
    void qtSlot( const std::string& message );
    // a Qt slot is a specially marked member function
    // a Boost slot is any callable signature
};

// Receiver.cpp
#include "Receiver.h"
#include <iostream>

void Receiver::qtSlot( const std::string& message )
{
    std::cout << message << std::endl;
}

 

下面,咱們來測試一下:

// main.cpp
#include <boost/bind.hpp>
#include "Sender.h"
#include "Receiver.h"

int main( int /*argc*/, char* /*argv*/[] )
{
    Sender* sender = new Sender;
    Receiver* receiver = new Receiver;

    // connect the boost style signal
    sender->boostSignal.connect(boost::bind(&Receiver::qtSlot, receiver, _1));
    // connect the qt style signal
    QObject::connect(sender, SIGNAL(qtSignal(const std::string&)),
                     receiver, SLOT(qtSlot(const std::string&)));
    sender->sendBoostSignal("Boost says 'Hello, World!'");
    sender->sendQtSignal("Qt says 'Hello, World!'");
    return 0;
}

 

這段代碼將會有相似下面的輸出:

[506]TestSignals$ ./TestSignals
Boost says 'Hello, World!'
Qt says 'Hello, World!'

 

咱們能夠看到,這兩種實現的不一樣之處在於,Boost.Signals 的信號,boostSignal,是 public 的,任何對象均可以直接發出這個信號。也就是說,咱們可使用以下的代碼:

sender->boostSignal("Boost says 'Hello, World!', directly");

 

從而繞過咱們設置的 sendBoostSignal() 這個觸發函數。另外,咱們能夠看到,boostSignal 徹底能夠是一個全局對象,這樣,任何對象均可以使用這個信號。而對於 Qt 來講,signal 必須是一個成員變量,在這裏,只有 Sender 可使用咱們定義的信號。

這個例子雖然簡單,然而已經很清楚地爲咱們展現了,如何經過 Qt 發出信號來獲取 Boost 的行爲。在這裏,咱們使用一個公共的 sendQtSignal() 函數發出 Qt 的信號。然而, 爲了從 Boost 的信號獲取 Qt 的行爲,咱們須要多作一些工做:隱藏信號,可是須要提供獲取鏈接的函數。這樣看上去有些麻煩:

class Sender : public QObject
{
    // just the changes...
private:
    // our new public connect function will be much easier to understand
    // if we simplify some of the type
    typedef boost::signal< void ( const std::string& ) > signal_type;
    typedef signal_type::slot_type slot_type;
    signal_type boostSignal;
    // our signal object is now hidden
public:
    boost::signals::connection
    connectBoostSignal( const slot_type& slot,
                        boost::signals::connect_position pos = boost::signals::at_back ) {
        return boostSignal.connect(slot, pos);
    }
};

 

應該說,這樣的實現至關醜陋。實際上,咱們將 Boost 的信號與鏈接分割開了。咱們但願可以有以下的實現:

// WARNING: no such thing as a connect_proxy
class Sender
{
public:
    connect_proxy< boost::signal< void ( const std::string& ) > >
    someSignal() {
        return someSignal_;
        // ...automatically wrapped in the proxy
    }
private:
    boost::signal< void ( const std::string& ) > someSignal_;
};
sender->someSignal().connect(someSlot);

 

注意,這只是個人但願,並無作出實現。若是你有興趣,不妨嘗試一下。

總結

前面囉嗦了這麼多,如今總結一下。

信號和槽的機制其實是觀察者模式的一種變形。它是面向組件編程的一種很強大的工具。如今,信號槽機制已經成爲計算機科學的一種術語,也有不少種不一樣的實現。

Qt 信號槽是 Qt 整個架構的基礎之一,所以它同 Qt 提供的組件、線程、反射機制、腳本、元對象機制以及可視化 IDE 等等緊密地集成在一塊兒。Qt 的信號是對象的成員函數,因此,只有擁有信號的對象才能發出信號。Qt 的組件和鏈接能夠由非代碼形式的資源文件給出,而且可以在運行時動態創建這種鏈接。Qt 的信號槽實現創建在 Qt 元對象機制之上。Qt 元對象機制由 Qt 提供的 moc 工具實現。moc 也就是元對象編譯器,它可以將用戶指定的具備 Q_OBJECT 宏的類進行必定程度的預處理,給這個增長元對象能力。

Boost.Signals 是具備靜態的類型安全檢查的,基於模板的信號槽系統的實現。全部的信號都是模板類 boost::signal 的一個特化;全部的槽函數都具備相匹配的可調用的簽名。Boost.Signals 是獨立的,不須要內省、元對象系統,或者其餘外部工具的支持。然而,Boost.Signals 沒有從資源文件動態創建鏈接的能力。

這兩種實現都很是漂亮,而且都具備工業強度。將它們結合在一塊兒使用也不是不可能的,Qt 4.1 即提供了這種可能性。

任何基於 Qt GUI 的系統都會天然而然的使用信號槽。你能夠從中獲取很大的好處。任何大型的系統,若是但願可以下降組件之間的耦合程度,都應該借鑑這種思想。正如其餘的機制和技術同樣,最重要的是把握一個度。在正確的地方使用信號槽,可讓你的系統更易於理解、更靈活、高度可重用,而且你的工做也會完成得更快。

相關文章
相關標籤/搜索