2013-01-22 39 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:很好的参考。玩得开心;) –