Thin C ++ signal / event mechanism with movement semantics for slots

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

First, let me give you some contextual information. I implemented a data processing system that is generated by various hardware sensors connected to a PC. Each individual hardware sensor is represented by a class that inherits from the universal Device class. Each sensor starts as a separate stream that receives data and can redirect it to several classes of processors (for example, filters, visualizers, etc.). In other words, the device is a signal, and the processor is a slot or listener. The whole system of signals / slots must be very efficient, since many data is generated by sensors.

The following code shows my first approach for signals with one argument. Additional template specializations can be added (copied) to enable support for additional arguments. Stream security is still missing in the code below (a mutex is required to synchronize access to slots_vec).

I wanted to make sure that each instance of the slot (i.e. the processor instance) cannot be used by another thread. So I decided to use unique_ptr and std :: move to implement the movement semantics for slots. This should ensure that if and only if the slots are turned off or when the signal is destroyed, the slots will also be destroyed.

I am wondering if this is an “elegant” approach. Any class using the Signal class below can either create an instance of Signal or inherit from Signal to provide 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; }; 

Other questions regarding this approach:

1) Typically, the signal and slots will use const references for complex data types as arguments. With boost :: signal, you need to use boost :: cref to feed the links. I would like to avoid this. 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 (the receiver should not inherit from the general slot type). In fact, you can associate any functor or function pointer. How it works? This can be useful if boost :: function is used to connect any function pointer or method pointer to a signal.

+1
source share
2 answers

ROOM:

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

This suggests that if your goal is more didactic and you want to play a little with these things to understand how they are implemented, I appreciate your spirit and try to answer your questions, but not before giving you tips for improvement.

CONSULTATIONS:

First of all, this sentence confuses:

"The connection and disconnection methods are still not thread safe. But I wanted to make sure that each instance of the slot (that is, the processor instance) cannot be used by another thread. I decided to use unique_ptr and std::move to implement the movement semantics for the slots."

Just in case, when you think about it (the “but” in your proposal assumes that), using unique_ptr does not save you from the need to protect your vector slots from data calculations. So you should still use the mutex to synchronize access to slots_vec anyway.

Second point: using unique_ptr you grant exclusive ownership of slot objects to a separate signal object. If I understand correctly, you declare that you are doing this in order to avoid using different streams associated with the same slot (which will force you to synchronize access to it).

I’m not sure that this is reasonable, reasonable. Firstly, it makes it impossible to register the same slot for several signals (I heard that you object that now you do not need it, but hold on). Secondly, you might want to change the state of these processors at runtime in order to adapt your response to the signals they receive. But if you do not have pointers to them, how would you do it?

Personally, at least I will go for shared_ptr , which will automatically control the lifetime of your slots; and if you don’t want multiple threads to mess up with these objects, just don’t give them access to them. Just avoid passing a generic pointer to these threads.

But I would go one step further : if your slots are callable objects, it seems to me, I would completely abandon shared_ptr and would rather use std::function<> to encapsulate them inside the Signal class. That is, I would just leave the vector objects std::function<> to call every time a signal is issued. That way, you would have more options than just inheriting from Slot to set up a callback: you could register a simple pointer to a function or the result of std::bind or just any functor you can think of (even a lambda).

Now you'll probably see that this is 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 more subtle than that; I'm just trying to show you why a modern library is designed this way and why it makes sense to resort to it at the end.

Of course, registering std::function objects, rather than smart pointers in your Signal class, will make you care about the lifetime of those functors that you allocate on the heap; however, it is not necessarily responsible for the Signal class. You can create a wrapper class for this purpose that can contain common pointers to functors created on the heap (for example, class instances obtained from Slot ) and register them in the Signal object. With some adaptation, it will also allow you to register and disable slots individually, rather than "all or nothing."

ANSWERS:

But let's now assume that your requirements will always be (the last part is really hard to foresee), for example:

  • You do not need to register the same slot for multiple signals;
  • You do not need to change the state of the slot at runtime;
  • You do not need to register different types of callbacks (lambdas, function pointers, functors, ...);
  • You do not need to selectively disable individual slots.

Then here are the answers to your questions:

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

A1: Yes, they will be referred to as permalinks because everything in your forwarding path is a permalink.

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

A2: it is based on the template of the class boost::function<> (which later became std::function and should be supported as such in VS2010, if I remember correctly), which uses the type of erase methods to wrap called objects of different types, but identical signatures. If you are interested in learning the implementation details, see the implementation of boost::function<> or look at the MS implementation of std::function<> (it should be very similar).

Hope this helps you a bit. If not, feel free to ask additional questions in the comments.

+2
source

Here is my approach:

This is much easier than boosting, but does not handle aggregate responses.

I think it's elegant to use shared_ptr for the callback owner and weak_ptr for the signal collector, which ensures that the callback still exists.

I also like the way it cleans dead weak_ptr callbacks.

 template <typename... FuncArgs> class Signal { using fp = std::function<void(FuncArgs...)>; std::forward_list<std::weak_ptr<fp> > registeredListeners; public: using Listener = std::shared_ptr<fp>; Listener add(const std::function<void(FuncArgs...)> &cb) { // passing by address, until copy is made in the Listener as owner. Listener result(std::make_shared<fp>(cb)); registeredListeners.push_front(result); return result; } void raise(FuncArgs... args) { registeredListeners.remove_if([&args...](std::weak_ptr<fp> e) -> bool { if (auto f = e.lock()) { (*f)(args...); return false; } return true; }); } }; 

Using:

 Signal<int> bloopChanged; // ... Signal<int>::Listener bloopResponse = bloopChanged.add([](int i) { ... }); 
0
source

All Articles