1

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.

spinxz
  • 401
  • 3
  • 13
  • 1
    Have you looked at [sigslot](http://sigslot.sourceforge.net/)? – paddy Jan 22 '13 at 13:40
  • For lifetime management of slots, boost::signals2 offers a method slot::track. See [here](http://stackoverflow.com/questions/14882867/boostsignals2-descruction-of-an-object-with-the-slot). – spinxz Apr 10 '13 at 13:52

2 Answers2

2

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.

Community
  • 1
  • 1
Andy Prowl
  • 124,023
  • 23
  • 387
  • 451
  • Thanks a lot for the elaborate and detailed answer. You are perfectly right that a mutex should be used to synchronize access to slots_vec. I edited my question for clarification. – spinxz Jan 22 '13 at 18:06
  • 1
    You are also right that usually an existing implementation should be used. A discussion about which signal/slot library to use can be found [here](http://stackoverflow.com/questions/359928/which-c-signals-slots-library-should-i-choose) and [here](http://www.kbasm.com/cpp-callback-benchmark.html). – spinxz Jan 22 '13 at 18:07
  • I have been using boost's signal2 library so far but it has be criticized for its low performance. The main reason for my own implementation is that I use smart pointers to manage signals and slot. But boost::signal's connect() does not work with (smart) pointers. Hence I had to dereference the pointer to the slot to connect it: `mysig.connect(*sharedPtr_to_my_slot)`. This is somewhat ugly and if the reference count is 0 and the signal might still use the slot... – spinxz Jan 22 '13 at 18:25
0

Here's my approach:

It's much lighter weight than boost but doesn't handle aggregated responses.

I think it's elegant in the use of shared_ptr for the owner of the callback, and weak_ptr for the signal raiser, which makes sure the callback is still around.

I also like how it self-cleans out the weak_ptr callbacks that are dead.

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

Usage:

Signal<int> bloopChanged;

// ...

Signal<int>::Listener bloopResponse = bloopChanged.add([](int i) { ... });
johnb003
  • 1,821
  • 16
  • 30