2013-01-22 38 views
1

我想在C++中設計一個信號和插槽系統。該機制有點受到boost :: signal的啓發,但應該更簡單。我正在使用MSVC 2010,這意味着一些C++ 11功能可用,但可悲的variadic模板不可用。Slim C++信號/事件機制與插槽的移動語義

首先,讓我給出一些上下文信息。我實現了一個系統來處理由連接到PC的不同硬件傳感器產生的數據。每個硬件傳感器都由一個繼承自通用類設備的類表示。每個傳感器作爲接收數據的單獨線程來運行,並且可以將其轉發給幾個類(例如過濾器,可視化器等)。換句話說,Device是一個信號,Processor是一個插槽或監聽器。整個信號/插槽系統應該非常高效,因爲傳感器會生成大量數據。

下面的代碼顯示了我的第一種方法的信號與一個參數。可以添加(複製)更多模板特化,以包括對更多參數的支持。以下代碼中缺少線程安全性(需要使用互斥鎖來同步對slots_vec的訪問)。

我想確保插槽(即處理器實例)的每個實例都不能被另一個線程使用。因此,我決定使用unique_ptr和std :: move來實現插槽的移動語義。這應該確保當且僅當插槽被斷開或信號被破壞時插槽也被破壞。

我想知道這是否是一種「優雅」的方法。任何使用下面的Signal類的類現在都可以創建一個Signal的實例或從Signal繼承來提供典型的方法(即連接,發射等)。

#include <memory> 
#include <utility> 
#include <vector> 

template<typename FunType> 
struct FunParams; 

template<typename R, typename A1> 
struct FunParams<R(A1)> 
{ 
    typedef R Ret_type; 
    typedef A1 Arg1_type; 
}; 

template<typename R, typename A1, typename A2> 
struct FunParams<R(A1, A2)> 
{ 
    typedef R Ret_type; 
    typedef A1 Arg1_type; 
    typedef A2 Arg2_type; 
}; 


/** 
Signal class for 1 argument. 
@tparam FunSig Signature of the Signal 
*/ 
template<class FunSig> 
class Signal 
{ 
public: 
    // ignore return type -> return type of signal is void 
    //typedef typenamen FunParams<FunSig>::Ret_type Ret_type; 
    typedef typename FunParams<FunSig>::Arg1_type Arg1_type; 

    typedef typename Slot<FunSig> Slot_type; 

public: 
    // virtual destructor to allow subclassing 
    virtual ~Signal() 
    { 
     disconnectAllSlots(); 
    } 

    // move semantics for slots 
    bool moveAndConnectSlot(std::unique_ptr<Slot_type> >& ptrSlot) 
    { 
     slotsVec_.push_back(std::move(ptrSlot)); 
    } 

    void disconnectAllSlots() 
    { 
     slotsVec_.clear(); 
    } 

    // emit signal 
    void operator()(Arg1_type arg1) 
    { 
     std::vector<std::unique_ptr<Slot_type> >::iterator iter = slotsVec_.begin(); 
     while (iter != slotsVec_.end()) 
     { 
      (*iter)->operator()(arg1); 
      ++iter; 
     } 
    } 

private: 
    std::vector<std::unique_ptr<Slot_type> > slotsVec_; 

}; 


template <class FunSig> 
class Slot 
{ 
public: 
    typedef typename FunParams<FunSig>::Ret_type Ret_type; 
    typedef typename FunParams<FunSig>::Arg1_type Arg1_type; 

public: 
    // virtual destructor to allow subclassing 
    virtual ~Slot() {} 

    virtual Ret_type operator()(Arg1_type) = 0; 
}; 

關於該方法另外的問題:

1)一般信號和槽將使用複雜的數據類型作爲參數常量的引用。使用boost :: signal需要使用boost :: cref來提供引用。我想避免這種情況。如果我按照如下方式創建一個Signal實例和一個Slot實例,是否保證參數是作爲const refs傳遞的?

class Sens1: public Signal<void(const float&)> 
{ 
    //... 
}; 

class SpecSlot: public Slot<Sens1::Slot_type> 
{ 
    void operator()(const float& f){/* ... */} 
}; 

Sens1 sens1; 
sens1.moveAndConnectSlot(std::unique_ptr<SpecSlot>(new SpecSlot)); 
float i; 
sens1(i); 

2)boost :: signal2不需要插槽類型(接收器不必從通用插槽類型繼承)。實際上可以連接任何函子或函數指針。這實際上是如何工作的?如果使用boost :: function將任何函數指針或方法指針連接到信號,這可能會很有用。

+1

你看過[sigslot](http://sigslot.sourceforge.net/)嗎? – paddy

+0

對於插槽的生命週期管理,boost :: signals2提供了一個方法slot :: track。見[這裏](http://stackoverflow.com/questions/14882867/boostsignals2-descruction-of-an-object-with-the-slot)。 – spinxz

回答

2

的前提:

如果您打算使用這是一個大的項目或在生產項目,我的第一個建議是不是推倒重來和而使用Boost.Signals2或備選庫。這些庫不像您想象的那麼複雜,並且可能比您可以想到的任何特定解決方案更有效率。

這就是說,如果你的目標是更多的教學種,你想玩這些東西瞭解他們是如何實現的,那麼我感謝你的精神,並會嘗試回答你的問題,但在給你一些改進建議之前不能。

建議:

首先,這句話是混亂:

的連接和斷開的方法不是線程安全的,到目前爲止我想確保每一個實例。 (即處理器實例)不能被另一個線程使用,因此我決定使用unique_ptrstd::move來實現槽「」的移動語義。

如果你正在考慮這個問題(你的句子中的「but」暗示了這一點),使用unique_ptr並不能保護你的數據競爭對手的vector。因此,您仍然應該使用互斥鎖來同步對slots_vec的訪問。

第二點:通過使用unique_ptr,您將槽對象獨佔所有權給單個信號對象。如果我理解正確,你聲稱你這樣做是爲了避免不同的線程搞亂同一個插槽(這會迫使你同步對它的訪問)。

我不確定這是否是設計明智的合理選擇。首先,它使不可能註冊多個信號(我聽到你反對,你不需要現在,但堅持)相同的插槽。其次,您可能希望在運行時更改這些處理器的狀態,以便使其反應適應所收到的信號。但是如果你沒有指向他們的話,你會怎麼做?

就我個人而言,我會至少去一個shared_ptr,這將允許自動管理您的插槽的生命週期;如果你不想讓多個線程搞砸這些對象,只是不要讓他們訪問它們。簡單地避免將共享指針傳遞給這些線程。

但是我走得一步:如果你的插槽可調用對象,因爲它似乎是,那麼我會在所有掉落shared_ptr和而使用std::function<>,把它們封裝在Signal類中。也就是說,每次發出信號時,我都會保留一個vectorstd::function<>對象。這樣你就可以有更多的選擇,而不僅僅是從Slot繼承來設置回調函數:你可以註冊一個簡單的函數指針,或者結果std::bind,或者任何你可以想到的函子(甚至是lambda)。

現在您可能已經看到這與Boost.Signals2的設計非常相似。請不要以爲我不會忽視這樣一個事實,即您最初的設計目標是要比這更薄;我只是想告訴你爲什麼最先進的圖書館是這樣設計的,以及爲什麼最終採用它是合理的。

當然,在您的Signal類中註冊對象而不是智能指針會強制您關注在堆上分配的函數的生命週期;然而,那不一定必須是Signal類的責任。你可以爲此創建一個包裝類,它可以保持指向你在堆上創建的函子的共享指針(比如派生自Slot的類的實例)並將它們註冊到Signal對象。通過一些改編,這也將允許您單獨註冊和斷開插槽而不是「全部或全部」。

解答:

但現在讓我們假設你的需求和永遠是(後半部分是真的很難預料)確實這樣認爲:

  1. 你不需要爲多個信號註冊相同的插槽;
  2. 您不需要在運行時更改插槽的狀態;
  3. 你不需要註冊不同類型的回調函數(lambda函數,函數指針,函數,...);
  4. 您不需要有選擇地斷開單個插槽。

然後這裏是問題的答案:

Q1:「[...]如果我創建一個信號實例和一個插槽的實例如下,是保證參數作爲傳遞const參考?「

A1:是的,它們會作爲常量引用傳遞,因爲沿着轉發路徑的所有內容都是常量引用。

Q2:「[Boost.Signals2]」可以實際連接任何函子或函數指針,這實際上是如何工作的?這可能是有用的,如果boost :: function用於連接任何函數指針或方法指針信號」

A2:它是基於boost::function<>類模板(後來成爲std::function,如果我記錯應該在VS2010被支撐爲這樣,),它使用type erasure techniques來包裝不同的類型,但相同的可調用的對象簽名。如果您對實施細節感興趣,請參閱implementation of boost::function<>或查看MS的實施std::function<>(應該非常相似)。

我希望這對你有所幫助。如果不是,請隨時在評論中提出其他問題。

+0

非常感謝您的詳細解答。你完全正確的是應該使用互斥鎖來同步對slots_vec的訪問。我編輯了我的問題進行澄清。 – spinxz

+1

你也是對的,通常應該使用現有的實現。關於使用哪個信號/插槽庫的討論可以在[這裏]找到(http://stackoverflow.com/questions/359928/which-c-signals-slots-library-should-i-choose)和[here]( http://www.kbasm.com/cpp-callback-benchmark.html)。 – spinxz

+0

@spinxz:很好的參考。玩得開心;) –