從零開始實現訊號槽機制:一

來源:互聯網
上載者:User

標籤:訊號槽   qt   sigslot   回調   

我們從一個具體的問題入手:

“現在有一堆按鈕,以及一堆電器,按鈕對它需要控制的對象一無所知,電器也不知道它們開關的具體類型,它們之間的關係可能是一對多,也可能是多對一,並且需要支援動態添加和刪除,應該如何設計這個結構?”

這裡有個形象的圖:



為了實現組件間的控制,我們很容易想到“回呼函數”,對於C++開發人員,我們肯定不希望一個類自身的處理函數存在於類外,但是類成員函數中被自動添加的隱形this形參造成了函數指標調用的不匹配,於是我們想到了使用static成員函數:

// 被調類class Tv{public:    static void onBtnClicked(bool b)    {        if ( b == true )            cout << "Tv is being turned on.";        else            cout << "Tv is being turned off.";    }};// 還有其他可能被調用的對象// class Lamp...// class Laptop...typedef void (*PF)(bool);class Button{public:    //主調函數    void click(PF p, bool b)    {        p(b);    }};int main(){    PF p = &Tv::onBtnClicked;    Button btn;    btn.click(p, true);    return 0;}

這樣我們就可以動態地為Button賦上它需要調用那個函數並執行。但缺點也是顯而易見的——被調函數作為靜態函數不能訪問非靜態成員與函數,Button觸發的結果對於每一個Tv對象都是一樣的,這顯然不是我們想要的。因此,我們想到了在Button中動態地傳入被調對象的指標,這樣,通過這個指標我們就可以調用該對象的非靜態成員函數了。由於Button還需要控制其他裝置,我們很自然地想到使用模板:

//被調類class Tv{public:    Tv(int t1, int t2) : bootTime(t1), offTime(t2){}    //被調函數    void onBtnClicked(bool b)    {        if ( b == true )            cout << "Tv is being turned on. bootTime is " << bootTime;        else            cout << "Tv is being turned off. offTime is " << offTime;    }private:    int bootTime;    int offTime;};// 還有其他可能被 Button 類控制的被調類class Lamp{public:    void onBtnClicked()    {        cout << "This Lamp is control by voice";    }};// class Laptop...// 主調類template<typename Tobject, typename Tparam>class Button{    typedef void (Tobject::*Tfunc)(Tparam);public:    Button(Tobject* pInstance, Tfunc p)    {        m_pInstance = pInstance;        m_pf = p;    }    //主調函數    void click(Tparam p)    {        (m_pInstance->*m_pf)(p);    }private:    Tfunc m_pf;    Tobject* m_pInstance;};int main(){    Tv tv1(20, 40);    Button<Tv, bool> btn(&tv1, &Tv::onBtnClicked);    btn.click(true);    return 0;}

這樣看起來還不錯,我們不僅將被調對象的類型獨立於Button,並且參數類型也是可變的(當然為了滿足一對多的關係,Button應該維護Tobject*和Tfunc的List。)。但我們依然沒有做到完全解耦:一、對於Button類中聲明的函數指標( typedef void (Tobject::*Tfunc)(Tparam) ),被調函數的傳回值與參數數量必須與其完全一致,但多個類型的被調函數可能不盡相同,例如Lamp::onBtnClicked();二、Button類掌握著所有被調對象的指標,這並不是一件安全的事情;三、被調對象先於調用被析構,將得到錯誤的結果。我們可以將main()中代碼改成下面這樣:

int main(){    Tv *tv1 = new Tv(20, 40);    Button<Tv, bool> btn(tv1, &Tv::onBtnClicked);    delete tv1;    btn.click(true);    return 0;}

危險的是,這是一個NULL指標調用成員函數的問題,很多時候編譯器並不對該情況報錯,於是我們得到了一個詭異的結果——列印了一個隨機的bootTime值。有時這種情況藏得很隱蔽,它可能將你拖入調試bug的沼澤。


之所以出現這樣的情況,是因為Tv類的完全獨立性,它沒有能力將自身被delete的資訊告訴Button,從而導致Button對其固執地調用;而Button類過多地關注被調對象與其函數指標也依然存在耦合,瞭解了問題所在,我們再來嘗試新的結構。首先我們把Button類中負責調用其他函數的內容剝離出來,用一個新的類Connection表示:

template<typename Tparam>class Connection_Base{public:    virtual MySlot* getdest() const = 0;    virtual void emitSignal(Tparam) = 0;    virtual ~Connection_Base(){}};template<typename Tobject, typename Tparam>class Connection : public Connection_Base<Tparam>{    typedef void (Tobject::*Tfunc)(Tparam);public:    Connection(Tobject* pobject, Tfunc p) {        m_pobject = pobject;        m_pf = p;    }    void emitSignal(Tparam parm) {        (m_pobject->*m_pf)(parm);    }    MySlot* getdest() const {        return m_pobject;    }private:    Tobject* m_pobject;    Tfunc m_pf;};

注意到這裡我們使用了兩個類,Connection和它的抽象基類Connection_Base,Connection所做的工作與我們從Button類中剝離出來的差不多,同樣維護著一個對象及其函數指標。之所以要添加這個基類Connection_Base是因為我們在發出訊號時是不關心接受者的類型的,因此只瞭解參數類型的訊號對象就可以只與Connection_Base進行互動。


接下來,為了讓“電器們”在銷毀時能夠及時解除它身上所有的綁定,最直接的方式是通知所有的主調類將它刪除。我們可以設計一個父類來完成這個工作,擁有槽函數的類只需要繼承它就好:

class MySlot{private:    typedef std::set<_Signal_Base*> sender_set;    typedef sender_set::const_iterator const_iterator;public:    void signal_connect(_Signal_Base* sender) {        m_senders.insert(sender);    }    void signal_disconnect(_Signal_Base* sender) {        m_senders.erase(sender);    }    virtual ~MySlot() {        disconnect_all();    }    void disconnect_all()    {        const_iterator it = m_senders.begin();        const_iterator itEnd = m_senders.end();        while(it != itEnd)        {            (*it)->slot_disconnect(this);            ++it;        }        m_senders.erase(m_senders.begin(), m_senders.end());    }private:    sender_set m_senders;};

即使現在我們還沒貼出_Signal_Base的代碼,相信 MySlot 這個類也依然十分容易理解,讓我們把每個訊號理解為一個對象,那麼 MySlot 則維護著一個儲存著所有當前已綁定的訊號的集合,在解構函式中我們調用disconnect_all(),調用每個訊號對象的slot_disconnect()函數移除自身。

有了連接者和槽,接下來我們就該實現Signal了:

class _Signal_Base{public:    virtual void slot_disconnect(MySlot* pslot) = 0;};
template<typename Tparam>class Signal_Base : public _Signal_Base{public:    typedef std::list<Connection_Base<Tparam>*>  connections_list;    ~Signal_Base() {        disconnect_all();    }    void disconnect_all()    {        typename connections_list::const_iterator it = m_connected_slots.begin();        typename connections_list::const_iterator itEnd = m_connected_slots.end();        while(it != itEnd)        {            (*it)->getdest()->signal_disconnect(this);            delete *it;            ++it;        }        m_connected_slots.erase(m_connected_slots.begin(), m_connected_slots.end());    }    void disconnect(MySlot* pclass)    {        typename connections_list::iterator it = m_connected_slots.begin();        typename connections_list::iterator itEnd = m_connected_slots.end();        while(it != itEnd)        {            if((*it)->getdest() == pclass)            {                delete *it;                m_connected_slots.erase(it);                pclass->signal_disconnect(this);                return;            }            ++it;        }    }    void slot_disconnect(MySlot* pslot)    {        typename connections_list::iterator it = m_connected_slots.begin();        typename connections_list::iterator itEnd = m_connected_slots.end();        while(it != itEnd)        {            typename connections_list::iterator itNext = it;            ++itNext;            if((*it)->getdest() == pslot)            {                m_connected_slots.erase(it);                //delete *it;            }            it = itNext;        }    }protected:    connections_list m_connected_slots;};

Signal_Base類維護了一個Connection的列表,由於每個新的連結都基於一個Connection,通過Connection的getdest()函數可以確定該連結綁定在哪個對象上,這樣,刪除某個Connection就可以刪除特定的連結了。最後的MySignal類是醬紫的:

template<typename Tparam>class MySignal : public Signal_Base<Tparam>{public:    template<typename Tobject>    void connect(Tobject* pclass, void (Tobject::*pmemfun)(Tparam))    {        Connection<Tobject, Tparam>* conn = new Connection<Tobject, Tparam>(pclass, pmemfun);        Signal_Base<Tparam>::m_connected_slots.push_back(conn);        pclass->signal_connect(this);    }    void operator()(Tparam p)    {        typename std::list<Connection_Base<Tparam>*>::const_iterator itNext, it = Signal_Base<Tparam>::m_connected_slots.begin();        typename std::list<Connection_Base<Tparam>*>::const_iterator itEnd = Signal_Base<Tparam>::m_connected_slots.end();        while(it != itEnd)        {            itNext = it;            ++itNext;            (*it)->emitSignal(p);            it = itNext;        }    }};

MySignal提供了connect()函數用來為新的連結產生Connection對象,然後重載了operator()使得我們可以使用“signal()”這種類似函數的形式來發出訊號。

一切就緒,寫段測試代碼看看:

class Button{public:    MySignal<bool> click;    void nowClick(bool b)    {        click(b);    }};class Tv : public MySlot{public:    Tv(int i) : m_id(i){}    void onButtonClicked(bool b)    {        if ( b == true )            cout << "Tv " << m_id << " is being turned on. " << endl;        else            cout << "Tv "<< m_id <<" is being turned off. " << endl;    }private:    int m_id;};int main(){    Button btn;    Tv *tv1 = new Tv(1);    Tv *tv2 = new Tv(2);    btn.click.connect(tv1, &Tv::onButtonClicked);    btn.click.connect(tv2, &Tv::onButtonClicked);    delete tv1;    btn.nowClick(false);    return 0;}

可以看到,Button類和Tv類現在可以徹底不再相互關心,而連結的工作交給客戶程式(main)去做,同時,在delete tv1之後,我們也不會碰到異常。完整的代碼實際就是把上面這幾塊揉在一起,就不再貼了,有興趣的話可以到博主的百度網盤下載:http://pan.baidu.com/s/1i3F8gkd


可能你已經注意到了,如果我們的訊號有兩個參數呢?是的,我們需要再寫一個有兩個參數的Signal_Base,Connection和MySignal。。。如果我們分別為0-8個參數寫了9個版本,並為這其中的容器操作加上安全執行緒機制,那麼我們現在完成的就是一份sigslot庫。

不知不覺篇幅有點長了~我們就留到下篇再來探討下,Qt中訊號槽的實現與sigslot又有哪些差異?以及moc和QObject又給訊號槽機制帶來了怎樣的靈活性~


著作權聲明:本文為博主原創文章,未經博主允許不得轉載。

從零開始實現訊號槽機制:一

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.