修身C ++信号/事件机制,此举语义插槽 [英] Slim c++ signal / event mechanism with move semantics for slots

查看:143
本文介绍了修身C ++信号/事件机制,此举语义插槽的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我试图设计C ++中的信号与槽系统。该机制在某种程度上被升压::信号启发而应该是简单。我与2010 MSVC的工作,这意味着一些C ++ 11功能可用但可悲的是可变参数模板都没有。

首先,让我提供一些背景信息。我实现的系统,用于处理由连接到个人电脑的不同硬件传感器产生的数据。每一个硬件传感器重新通过从泛型类的设备的继承的类psented $ P $。每个传感器被运行作为接收数据,并且可以将其转发给几个<青霉>处理器的类一个单独的线程(例如过滤器,可视化工具,等)。换言之,一个设备是一个信号和一个处理器是一个时隙或听众。如由传感器所产生的大量的数据的整个信号/槽系统应该是非常有效的。

以下code显示我的一个参数信号的第一种方法。更多的模板特可以添加(复制)到包括更多的论据支持。线程安全在下面的code,到目前为止失踪(互斥体将需要同步访问slots_vec)。

我想确保一个时隙(即处理器实例)的每个实例不能被其他线程使用。因此,我决定使用的unique_ptr和std ::移动到落实插槽移动语义。这应该确保,当且仅当插槽断开或当信号被破坏的插槽得到破坏为好。

我想知道如果这是一个优雅的方式。使用下面的信号类现在可以创建信号的实例或继承的信号,提供典型的方法,任何类(即连接,排出等)。

 的#include&LT;内存和GT;
#包括LT&;实用&GT;
#包括LT&;矢量&GT;模板&LT; typename的FunType&GT;
结构FunParams;模板&LT; typename的R,typename的A1&GT;
结构FunParams&LT; R(A1)&GT;
{
    的typedef - [R Ret_type;
    A1的typedef Arg1_type;
};模板&LT; typename的R,typename的A1,A2类型名称和GT;
结构FunParams&所述R(A1,A2)与GT;
{
    的typedef - [R Ret_type;
    A1的typedef Arg1_type;
    A2的typedef Arg2_type;
};
/ **
信号等级为1的说法。
信号@tparam FunSig签名
* /
模板&LT;类FunSig&GT;
类信号
{
上市:
    //忽略返回类型 - &GT;信号的返回类型为void
    // typedef的typenamen FunParams&LT; FunSig&GT; :: Ret_type Ret_type;
    的typedef typename的FunParams&LT; FunSig&GT; :: Arg1_type Arg1_type;    的typedef typename的插槽&LT; FunSig&GT; Slot_type;上市:
    //虚析构函数允许子类化
    虚拟〜信号()
    {
        disconnectAllSlots();
    }    //向前冲槽语义
    布尔moveAndConnectSlot(的std ::的unique_ptr&LT; Slot_type&GT;&GT;&安培; ptrSlot)
    {
        slotsVec_.push_back(性病::移动(ptrSlot));
    }    无效disconnectAllSlots()
    {
        slotsVec_.clear();
    }    //发出信号
    void运算符()(Arg1_type ARG1)
    {
        的std ::矢量&lt;的std ::的unique_ptr&LT; Slot_type&GT; &GT; ::迭代器ITER = slotsVec_.begin();
        而(ITER!= slotsVec_.end())
        {
            (* ITER) - GT;运营商()(ARG1);
            ITER ++;
        }
    }私人的:
    的std ::矢量&lt;的std ::的unique_ptr&LT; Slot_type&GT; &GT; slotsVec_;};
模板&LT;类FunSig&GT;
类插槽
{
上市:
    的typedef typename的FunParams&LT; FunSig&GT; :: Ret_type Ret_type;
    的typedef typename的FunParams&LT; FunSig&GT; :: Arg1_type Arg1_type;上市:
    //虚析构函数允许子类化
    虚拟〜插槽(){}    虚拟Ret_type符()(Arg1_type)= 0;
};

对于这种方法的其他问题:

1)通常情况下,信号和槽将使用复杂的数据类型作为参数常量引用。随着的boost ::信号则需要使用boost :: CREF养活引用。我想避免这种情况。如果我创建一个信号实例和插槽的实例如下,它是保证该参数为const裁判通过呢?

 类SENS1:公共信号u;无效(常量浮动&放大器;)&GT;
{
  // ...
};类SpecSlot:公共插槽&LT; SENS1 :: Slot_type&GT;
{
   void运算符()(常量浮球放; F){/ * ... * /}
};SENS1 SENS1;
sens1.moveAndConnectSlot(性病::的unique_ptr&LT; SpecSlot&GT;(新SpecSlot));
浮动我;
SENS1(ⅰ);

2)升压:: SIGNAL2不需要槽型(一个接收器不具有以从通用槽类型继承)。一个实际上可以连接任何算符或函数指针。这是如何实际工作?如果升压::函数用于连接任何函数指针或方法指针以一个信号,这可能是有用的。


解决方案

preMISE:

如果您打算使用这是一个大的项目或者生产项目,我的第一个建议是为不是推倒重来并愿意使用Boost.Signals2或替代库。这些库并不像你想象的那么复杂,很可能是更有效的比任何的特设的解决方案,你能想出。

这就是说,如果你的目标是多的​​说教的善良,你要玩了一下这些事情弄清楚它们是如何实现的,然后我AP preciate你的精神并会尽量回答你的问题,但不给你一些之前的的改进建议

建议:

首先,这句话是令人困惑的:

的连接和断开方法不是线程安全的,到目前为止,但是我想确保插槽(即处理器实例)的每个实例不能被其他线程使用。因此,我决定用的unique_ptr 的std ::移动来落实插槽移动语义的。

万一你考虑它(,而是在你的句子表明),使用的unique_ptr 并没有真正节省您不必保护您的矢量对数据竞争的插槽。因此,你还是应该使用一个互斥同步访问 slots_vec 反正。

第二点:利用的unique_ptr ,你给插槽对象单独的信号对象的独占所有权。如果我理解正确的话,你声称你这样做是为了避免不同的线程搞乱用相同的插槽(这将迫使你同步访问它)。

我不知道这是设计的角度来看,一个合理的选择。首先,这使得它不可能注册的多个信号相同的插槽(我听到你反对,你不需要那么的现在的,但坚持)。其次,你可能要改变这些处理器的状态下运行时使自己的反应适应他们收到的信号。但是,如果你没有指针给他们,你会怎么做呢?

我个人的至少的走了的shared_ptr ,这样可以让你的插槽'生命周期的自动化管理​​;如果你不想多线程乱用这些对象,就是不给他们访问它们。简单地避免了共享指针传递给这些线程。

不过,我会去,甚至一步:如果你的插槽的调用对象的,因为它似乎是,那么我将放弃的shared_ptr 在所有和宁愿使用的std ::功能&LT;&GT; 来封装它们的信号里面类。也就是说,我只想保持性病的矢量 ::功能&LT;&GT; 对象调用每次信号被发射。这样,你就必须比插槽,以建立一个回调只是继承了更多的选择:你可以​​注册一个简单的函数指针,或的std ::绑定,或者只是任何函子可以拿出(甚至一个lambda)。

现在你可能看到,这已变得越来越相似,Boost.Signals2的设计。请不要认为我没有忽视一个事实,即原来的设计目标是有一些较苗条;我只是想告诉你为什么国家的最先进的图书馆就是这样设计的,为什么它是有道理求助于它到底。

当然,注册的std ::功能对象,而不是在你的信号类将迫使你采取智能指针关心那些你在堆上分配仿函数的寿命;然而,这并不不一定必须是信号类的责任。您可以创建用于该目的的包装类,可以保持共享指针你在堆上创建的仿函数(比如从插槽派生的类的实例),并在注册它们信号对象。对于一些适应,这也将让你注册并断开插槽的个别的,而不是全有或全无。

解答:

不过,现在让我们假设你的需求和永远是(后半部分是真的很难预料)确实是这样的:


  1. 您并不需要注册多个信号相同的插槽;

  2. 您不需要在运行时更改插槽的状态;

  3. 您并不需要注册不同类型的回调(lambda表达式,函数指针,仿函数,...);

  4. 您不需要选择断开单个插槽。

然后这里是问题的答案:

Q1:?[...]如果我创建一个信号实例和插槽的实例如下,它是保证该参数为const裁判通过

A1:是的,他们会不断的引用传递,因为沿着你的转发路径的一切是一个常数参考

Q2:[在Boost.Signals2]可以真正连接任何仿函数的函数指针这是如何实际工作,这可能提振是否::功能是用来连接任何函数指针或方法指针是有用的?到一个信号

A2:这是根据的boost ::功能&LT;&GT; 类模板(即后来的的std ::功能,如果我没记错的话应该在VS2010支持正因为如此,),它使用类型擦除技术包装不同类型但相同的签名可调用对象。如果你好奇的实施细则,请参阅<$的实施C $ C>的boost ::功能&LT;&GT; 或看看MS的执行的std ::功能&LT的;&GT; (应该是非常相似)。

我希望这有助于你一点。如果没有,随意问其他问题的意见。

I am trying to design a signal and slot system in c++. The mechanism is somewhat inspired by boost::signal but should be simpler. I am working with MSVC 2010 which means that some c++11 features are available but sadly variadic templates are not.

First, let me give some contextual information. I implemented a system for processing data that is generated by different hardware sensors connected to the pc. Every single hardware sensor is represented by a class that inherits from a generic class Device. Every sensor is run as a separate thread which receives the data and can forward it to several Processor classes (e.g. filters, visualizers, etc.). In other words, a Device is a signal and a Processor is a slot or listener. The whole signal/slot system should be very efficient as a lot of data is generated by the sensors.

The following code shows my first approach for signals with one argument. More template specializations can be added (copied) to include support for more arguments. The thread safety is missing so far in the code below (a mutex would be required to synchronize access to slots_vec).

I wanted to make sure that every instance of a slot (i.e. a processor instance) cannot be used by another thread. Hence I decided to use unique_ptr and std::move to implement move semantics for slots. This should make sure that if and only if the slots are disconnected or when the signal is destructed the slots get destructed as well.

I am wondering if this is an "elegant" approach. Any class using the Signal class below can now either create an instance of Signal or inherit from Signal to provide the typical methods (i.e. connect, emit, etc.).

#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;
};

Further questions regarding this approach:

1) Usually the signal and slots will use const references to complex data types as arguments. With boost::signal it is required to use boost::cref to feed references. I would like to avoid that. If I create a Signal instance and a Slot instance as follows, is it guaranteed that the arguments are passed as 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 does not require a slot type (a receiver does not have to inherit from a generic slot type). One can actually connect any functor or function pointer. How does this actually work? This might be useful if boost::function is used to connect any function pointer or method pointer to a signal.

解决方案

PREMISE:

If you plan to use this is in a large project or in a production project, my first suggestion is to not reinvent the wheel and rather use Boost.Signals2 or alternative libraries. Those libraries are not as complicated as you might think, and are likely to be more efficient than any ad hoc solution you could come up with.

This said, if your goal is more of a didactic kind, and you want to play a bit with these things to figure out how they are realized, then I appreciate your spirit and will try to answer your questions, but not before giving you some advice for improvement.

ADVICES:

First of all, this sentence is confusing:

"The connect and disconnect methods are not thread safe so far. But I wanted to make sure that every instance of a slot (i.e. a processor instance) cannot be used by another thread. Hence I decided to use unique_ptr and std::move to implement move semantics for slots".

Just in case you're thinking about it (the "but" in your sentence suggests that), using unique_ptr does not really save you from having to protect your vector of slots against data races. Thus, you should still use a mutex to synchronize access to slots_vec anyway.

Second point: by using unique_ptr, you give exclusive ownership of the slot objects to individual signal object. If I understand correctly, you claim you are doing this to avoid different threads messing up with the same slot (which would force you to synchronize access to it).

I'm not sure this is, design-wise, a reasonable choice. First of all, it makes it impossible to register the same slot for multiple signals (I hear you objecting that you don't need that now, but hold on). Secondly, you might want to change the state of those processors at run-time so to adapt their reaction to the signals they receive. But if you have no pointers to them, how would you do that?

Personally, I would at least go for a shared_ptr, which would allow automatic management of your slots' lifetime; and if you don't want multiple threads to mess up with those objects, just don't give them access to them. Simply avoid passing the shared pointer to those threads.

But I'd go even one step further: if your slots are callable objects, as it seems to be, then I would drop the shared_ptr at all and rather use std::function<> to encapsulate them inside the Signal class. That is, I would just keep a vector of std::function<> objects to be invoked each time a signal is emitted. This way you would have more options than just inheriting from Slot in order to set up a callback: you could register a simple function pointer, or the result of std::bind, or just any functor you can come up with (even a lambda).

Now you probably see that this is getting very similar to the design of Boost.Signals2. Please do not think that I am not ignoring the fact that your original design goal was to have something slimmer than that; I'm just trying to show you why a state-of-the-art library is designed that way and why it makes sense to resort to it in the end.

Certainly, registering std::function objects rather than smart pointers in your Signal class would force you to take care about the lifetime of those functors you allocate on the heap; however, that does not necessarily have to be a responsibility of the Signal class. You can create a wrapper class for that purpose, that could keep shared pointers to the functors you create on the heap (like instances of classes derived from Slot) and register them at the Signal object. With some adaptation, this would also allow you to register and disconnect slots individually rather than "all or nothing".

ANSWERS:

But let's now suppose that your requirements are and will always be (the latter part is really hard to foresee) indeed such that:

  1. You do not need to register the same slot for multiple signals;
  2. You do not need to change the state of a slot at run-time;
  3. You do not need to register different types of callbacks (lambdas, function pointers, functors, ...);
  4. You do not need to selectively disconnect individual slots.

Then here are the answers to your questions:

Q1: "[...] If I create a Signal instance and a Slot instance as follows, is it guaranteed that the arguments are passed as const refs?"

A1: Yes, they will be passed as constant references, because everything along your forwarding path is a constant reference.

Q2: "[In Boost.Signals2] one can actually connect any functor or function pointer. How does this actually work? This might be useful if boost::function is used to connect any function pointer or method pointer to a signal"

A2: It is based on the boost::function<> class template (which later became std::function and should be supported as such in VS2010, if I remember correctly), which uses type erasure techniques to wrap callable objects of different types but identical signatures. If you are curious about the implementation details, see the implementation of boost::function<> or have a look at MS's implementation of std::function<> (should be very similar).

I hope this helped you a bit. If not, feel free to ask additional questions in the comments.

这篇关于修身C ++信号/事件机制,此举语义插槽的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆