Implementing signals with variadic templates

One of the most popular patterns in modern C++ is the use of signals. Signals are an implementation of the observer pattern, where observers register, or connect themselves to a signal, so that the signal can notify them when it has been triggered. This paradigm has been used in a lot of software, with Qt's signal/slot system probably being the most widely known.

The signal is usually called by using operator () and passing a certain number of arguments. Observers usually implement a function that matches the signature of the operator () and when operator () is called, each function of the registered observers is called with the same arguments.

Modern C++ allows us to define a generic signal in a very straightforward manner by using variadic templates. Generic means: The signal class is written once and the template instantiation takes care of defining the different parameters and types that may be passed to the signal.
In the traditional definition of the observer pattern, a signal stores pointers to Observer objects, and Observer is an interface that defines the virtual method that needs to be called. Finally, real observers extend/inherit from the Observer class and implement that method.

However, in modern C++, the use of std::function means that we no longer have to define any particular method on an observer object itself. Rather, a lambda can take care of calling any function on any object for us. This means that our objects no longer need to be Observer subclasses and their definition does not need to be polluted just so they can be connected to a certain signal.

In any case, let's show some code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <stdint.h>
#include <vector>
#include <functional>
#include <algorithm>

template <typename ...T> class CSignal
{
 public:
  void connect(intptr_t id, std::function <void(T...)> func)
  {
   auto iter = std::find_if(m_observers.begin(), m_observers.end(),
    [id] (const Observer& obs) -> bool
    {
     return obs.id == id;
    }
   );

   if (iter != m_observers.end())
   {
    iter->func = func;
   }
   else
   {
    Observer newObs = {id, func};
    m_observers.push_back(newObs);
   }
  }

  void disconnect(intptr_t id)
  {
   m_observers.erase(std::remove_if(m_observers.begin(), m_observers.end(),
   [id] (const Observer& obs) -> bool
   {
    return obs.id == id;
   }), m_observers.end());
  }

  void operator () (T... args)
  {
   // by iterating in reverse, we guarantee that any observer that
   // wants to disconnect during the signal, can do so
   for (int i = static_cast<int> (m_observers.size()) - 1; i >=0; --i)
   {
    m_observers[i].func(args...);
   }
  }

 private:
  struct Observer
  {
   intptr_t id;
   std::function <void(T...)> func;
  };

  std::vector <Observer> m_observers;
}; 


As you can see, we can connect any std::function in our CSignal and the template instantiation mechanism will make sure that our arguments match. In operator (), the arguments passed to the signal are expanded and passed directly to our observer std::functions.

Of course it is possible to add some convenience code in our signal class so that we can directly connect an object and a method pointer, without the need for client code to define a lambda, but for now this is left as an exercise for the reader.

To define a signal, we simply add the signal signature types in the template instantiation parameters like so:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void respondToEvent(Event& evt)
{
    // code that responds to the event
}

CSignal <Event&> mySignal;
// connect to the signal using some id
mySignal.connect(id, respondToEvent);

// Notify observers using the signal
Event e;
mySignal(e);

A.R.

Comments

Popular posts from this blog

Logarithmic scales for UI

Improving the performance of UI list updates

Contexted drop in Rust