Slim c ++信号/事件机制,带有槽的移动语义 [英] Slim c++ signal / event mechanism with move semantics for slots

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

问题描述

我想在c ++中设计一个信号和槽系统。该机制有点灵感来自boost :: signal,但应该更简单。我正在使用MSVC 2010,这意味着一些c ++ 11的功能是可用的,但可悲的是可变的模板不是。



首先,让我给一些上下文信息。我实现了一个系统来处理由连接到pc的不同硬件传感器生成的数据。每个单独的硬件传感器由继承自通用类设备的类表示。每个传感器作为接收数据的单独线程运行,并且可以将其转发到若干处理器类(例如过滤器,可视化器等)。换句话说,设备是信号,处理器是插槽或监听器。整个信号/槽系统应该非常有效,因为传感器产生了大量的数据。



下面的代码显示了我的第一种方法。可以添加(复制)更多模板特化,以包含对更多参数的支持。到目前为止,下面的代码中缺少线程安全性(需要互斥锁来同步对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;
};


/ **
1个参数的信号类。
@tparam FunSig信号签名
* /
template< class FunSig>
class Signal
{
public:
// ignore return type - >返回信号类型为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〜Signal()
{
disconnectAllSlots();
}

//移动槽的语义
bool moveAndConnectSlot(std :: unique_ptr< Slot_type>>& ptrSlot)
{
slotsVec_。 push_back(std :: move(ptrSlot));
}

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

//发出信号
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〜Slot(){}

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

有关此方法的其他问题:



1)通常,信号和槽将使用对复杂数据类型的const引用作为参数。使用boost :: signal,需要使用boost :: cref来引用引用。我想避免这样。如果我创建一个Signal实例和一个Slot实例如下,是否保证参数传递为const refs?

  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用于连接任何函数指针或方法指针到一个信号。

解决方案

PREMISE:



如果您计划在大型项目或生产项目中使用,我的第一个建议是,而使用Boost.Signals2或替代库。这些库并不像你想象的那么复杂,并且可能比你想出的任何一种特别的解决方案更有效率。



这就是说,如果你的目标更多地是一个教学,并且你想玩这些东西来弄清楚它们是如何实现的,那么我感谢你的精神,并将尽力回答你的



b

首先,这句话是混乱的:



到目前为止,connect和disconnect方法不是线程安全的。但是我想确保一个插槽的每个实例(即一个处理器实例)不能被另一个线程使用,因此我决定使用 unique_ptr std :: move 可为插槽实施移动语义。



思考它(你的句子中的但)表明,使用 unique_ptr 不能真正地保护你不必保护你的向量的数据竞争。因此,您仍然应该使用互斥锁同步访问 slots_vec



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



我不确定这是否是设计方面的合理选择。首先,它不能为多个信号注册相同的插槽(我听说您反对,现在不需要 ,但按住)。其次,您可能希望在运行时更改这些处理器的状态,以便使其响应适应其接收的信号。



我个人至少会 去找一个 shared_ptr ,这将允许自动管理您的插槽的生命周期;如果你不想让多个线程搞砸那些对象,只是不让他们访问它们。



但是我会进一步进一步:如果你的槽是可调用对象,因为它似乎是,然后我会删除 shared_ptr 在所有,而不是使用 std :: function< / code>将它们封装在 Signal 类中。也就是说,我每次保存一个向量 std :: function<> 信号。这样,您将拥有更多的选项,而不仅仅是继承 Slot 以便设置回调:您可以注册一个简单的函数指针,或 std :: bind ,或者只是任何可以提出的函数(甚至一个lambda)。



现在你可能看到变得非常类似于Boost.Signals2的设计。请不要认为我并不忽视这样的事实,你原来的设计目标是要有一些比这更微小的东西;我只是想告诉你为什么一个最先进的图书馆是这样设计的,为什么最后诉诸它是有道理的。



当然,在信号类中注册 std :: function 对象而不是智能指针会迫使你注意你在堆上分配的那些函子的生命周期;然而,不一定必须是 Signal 类的责任。你可以创建一个包装类来保存你在堆上创建的函数的共享指针(例如从 Slot 派生的类实例),并将它们注册到信号对象。



答案:



但现在让我们假设您的要求是总是(后半部分真的很难预见) :


  1. 您不需要为多个信号注册同一个广告位;

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

  3. 您不需要注册不同类型的回调(lambdas,函数指针,函子...);
  4. li>
  5. 您不需要选择性地断开个别插槽。



Q1:[...]如果我创建一个Signal实例和一个Slot实例如下,它保证参数传递为const



A1:是的,它们将作为常量引用传递,因为您的转发路径上的所有内容都是常量引用。



Q2:在Boost.Signals2中,实际上可以连接任何函数或函数指针。这实际上如何工作?这可能是有用的,如果boost :: function用于连接任何函数指针或方法指针到一个信号



A2: code> boost :: function<> 类模板(后来成为 std :: function ,应该支持VS2010 ,如果我记得正确),它使用类型擦除技术封装不同类型但具有相同签名的可调用对象。如果您对实施详情感兴趣,请参阅实施 boost :: function<> 或者查看MS的 std :: function<>



我希望这可以帮助你一点,如果没有,请在评论中提出其他问题。


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.

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

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