1. 程式人生 > >[轉]深入理解信號槽機制

[轉]深入理解信號槽機制

困難 creat nec 保持 ssa 指針傳遞 ech 消費 導致

原文不可考

來源鏈接http://blog.csdn.net/liuuze5/article/details/53523463

深入理解信號槽(一)

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

什麽是信號槽?

這個問題我們可以從兩個角度來回答,一個簡短一些,另外一個則長些。

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

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

信號和槽是多對多的關系。一個信號可以連接多個槽,而一個槽也可以監聽多個信號。

信號可以有附加信息。例如,窗口關閉的時候可能發出 windowClosing 信號,而這個信號就可以包含著窗口的句柄,用來表明究竟是哪個窗口發出這個信號;一個滑塊在滑動時可能發出一個信號,而這個信號包含滑塊的具體位置,或者新的值等等。我們可以把信號槽理解成函數簽名。信號只能同具有相同簽名的槽連接起來。你可以把信號看成是底層事件的一個形象的名字。比如這個 windowClosing 信號,我們就知道這是窗口關閉事件發生時會發出的。

信號槽實際是與語言無關的,有很多方法都可以實現信號槽,不同的實現機制會導致信號槽差別很大。信號槽這一術語最初來自 Trolltech 公司的 Qt 庫(現在已經被 Nokia 收購)。1994年,Qt 的第一個版本發布,為我們帶來了信號槽的概念。這一概念立刻引起計算機科學界的註意,提出了多種不同的實現。如今,信號槽依然是 Qt 庫的核心之一,其他許多庫也提供了類似的實現,甚至出現了一些專門提供這一機制的工具庫。

簡單了解信號槽之後,我們再來從另外一個角度回答這個問題:什麽是信號槽?它們從何而來?

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

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

// 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 的系統都會自然而然的使用信號槽。你可以從中獲取很大的好處。任何大型的系統,如果希望能夠降低組件之間的耦合程度,都應該借鑒這種思想。正如其他的機制和技術一樣,最重要的是把握一個度。在正確的地方使用信號槽,可以讓你的系統更易於理解、更靈活、高度可重用,並且你的工作也會完成得更快。

[轉]深入理解信號槽機制