I came up with my own solution and I think it's quite flexible. It does have one small drawback, though: It uses the heap. This was a no-no for me but I couldn't find a way around that. I have attached the header and here is the walkthrough:
First, the upper half of the header guard and includes:
Code:
#ifndef _SIGNALS_H_
#define _SIGNALS_H_
#include <utility>
We need std::forward from <utility> to perfectly forward arguments to the object that will be connected to a signal.
The first class we need is a delegate interface that provides a call operator with any number and type of arguments:
Code:
/** Interface for delegates with a specific set of arguments **/
template<typename... args>
class AbstractDelegate
{
public:
virtual void operator()(args&&...) const = 0;
virtual ~AbstractDelegate() {}
};
The first kind of concrete delegate is for non-static member functions. It stores an object of any type and a pointer to a member function that can return any type and accept any number of types:
Code:
/** Concrete member function delegate that discards the function's return value **/
template<typename T, typename ReturnType, typename... args>
class ObjDelegate : public AbstractDelegate<args...>
{
public:
/** member function typedef **/
typedef ReturnType (T::*ObjMemFn)(args...);
/** constructor **/
ObjDelegate(T& obj, ObjMemFn memFn)
: obj_(obj),
memFn_(memFn)
{
}
/** call operator that calls the stored function on the stored object **/
void operator()(args&&... a) const
{
(obj_.*memFn_)(std::forward<args>(a)...);
}
private:
/** reference to the object **/
T& obj_;
/** member function pointer **/
const ObjMemFn memFn_;
};
The second concrete delegate class is used for free functions and static member functions:
Code:
/** Concrete function delegate that discards the function's return value **/
template<typename ReturnType, typename... args>
class FnDelegate : public AbstractDelegate<args...>
{
public:
/** member function typedef **/
typedef ReturnType (*Fn)(args...);
/** constructor **/
FnDelegate(Fn fn)
: fn_(fn)
{
}
/** call operator that calls the stored function **/
void operator()(args&&... a) const
{
(*fn_)(std::forward<args>(a)...);
}
private:
/** function pointer **/
const Fn fn_;
};
No let's have a look at the other side of things. A Signal can call any number of delegates, which are in turn referenced by a Connection - so a signal stores a list of connections. As I don't like the standard containers for embedded work, the list of connections is implemented as an intrusive linked list. This makes a forward declaration of the connection class necessary:
Code:
/** forward declaration **/
template<typename... args>
class Connection;
/** Signal class that can be connected to**/
template<typename... args>
class Signal
{
public:
/** connection pointer typedef **/
typedef Connection<args...>* connection_p;
/** constructor **/
Signal()
: connections_(NULL)
{
}
/** call operator that calls all connected delegates.
The most recently connected delegate will be called first **/
void operator()(args&&... a) const
{
auto c = connections_;
while(c != NULL)
{
(c->delegate())(std::forward<args>(a)...);
c = c->next();
}
}
/** connect to this signal **/
void connect(connection_p p)
{
p->next_ = connections_;
connections_ = p;
p->signal_ = this;
}
/** disconnect from this signal.
Invalidates the connection's signal pointer
and removes the connection from the list **/
void disconnect(connection_p conn)
{
// find connection and remove it from the list
connection_p c = connections_;
if (c == conn)
{
connections_ = connections_->next();
conn->next_ = NULL;
conn->signal_ = NULL;
return;
}
while(c != NULL)
{
if (c->next() == conn)
{
c->next_ = conn->next();
conn->next_ = NULL;
conn->signal_ = NULL;
return;
}
c = c->next();
}
}
/** destructor. disconnects all connections **/
~Signal()
{
connection_p p = connections_;
while(p != NULL)
{
connection_p n = p->next();
disconnect(p);
p = n;
}
}
private:
connection_p connections_;
};
That was a lot of code, but the most complicated parts actually just manage the list of connections. The Signal's call operator loops through all connections and calls all delegates.
Connecting to the Signal is simple: The connection is added to the list, and the connection's signal pointer (see below) is set to this signal.
Disconnecting from a signal is a bit more complicated, as an entry needs to be removed from a singly linked list.
The signal destructor disconnects all connections which are still connected to this signal. The object owning the connection can use the connection's connected() method to see if the signal it refers to is still alive. I don't think if this is very useful, but it was easy to implement.
The Connection class manages a delegate and connects it to a signal. The constructor is templated to allow for non-static and static (or free) functions, making use of the heap necessary: On construction is it known what exact type of delegate is needed, and such a delegate is allocated on the heap. I don't think this is too bad for two reasons:
- a delegate only stores one or two pointers
- most embedded applications are more or less static, so no (or few) delete() calls will be made during runtime
Let's have a look:
Code:
/** connection class that can be connected to a signal **/
template<typename... args>
class Connection
{
public:
/** template constructor for non-static member functions.
allocates a new delegate on the heap **/
template<typename T, typename ReturnType>
Connection(Signal<args...>* signal, T& obj, ReturnType (T::*memFn)(args...))
: delegate_(new ObjDelegate<T, ReturnType, args...>(obj, memFn)),
signal_(NULL),
next_(NULL)
{
signal->connect(this);
}
/** template constructor for static member functions and free functions.
allocates a new delegate on the heap **/
template<typename ReturnType>
Connection(Signal<args...>* signal, ReturnType (*Fn)(args...))
: delegate_(new FnDelegate<ReturnType, args...>(Fn)),
signal_(NULL),
next_(NULL)
{
signal->connect(this);
}
/** get reference to this connection's delegate **/
AbstractDelegate<args...>& delegate() const
{
return *delegate_;
}
/** get pointer to next connection in the signal's list **/
Connection* next() const
{
return next_;
}
/** is this connection connected to a valid signal? **/
bool connected() const
{
return (signal_ != NULL);
}
/** desctructor. If the signal is still alive, disconnects from it **/
~Connection()
{
if (signal_ != NULL)
{
signal_->disconnect(this);
}
delete delegate_;
}
friend class Signal<args...>;
private:
AbstractDelegate<args...>* delegate_;
Signal<args...>* signal_;
Connection* next_;
};
Construction really is very simple: The appropriate delegate is constructed an the Connection is added to the given Signal. The delegate(), next() and connected() methods are trivial, they provide information about the connection and the list of Connections for the Signal. The destructor is more interesting: if the Signal is still alive, the connection is removed from it. Then the delegate which was allocated by the constructor is deleted.
So how can we use this? Here's an example:
Code:
/** An Act object can be incremented and provides a Signal that can be used to be notified when that happens: **/
class Act
{
public:
Act() : val_(0) {}
void increment()
{
val_++;
incremented(val_);
incremented(5);
incremented(val());
}
Signal<const int&> incremented;
private:
int val() {return val_;}
int val_;
};
/** A Watch object watches an Act object, automatically printing the Act objects' signalled value **/
class Watch
{
public:
Watch(uint8_t offset = 0)
: offset_(offset)
{
}
void print(const int& i)
{
Serial.print("Watch::print(");Serial.print(i+offset_);Serial.println(")");
}
private:
uint8_t offset_;
};
Act act;
Watch watch(4);
Connection<const int&> conn(&act.incremented, watch, &Watch::print);
void loop()
{
act.increment();
delay(500);
}
A warning: Be very careful when choosing the signal and connection parameters. The above example works with incremented(val_) and with incremented(5) only because the argument type is const int&. If you remove constness or referenceness from the template parameter, one of the calls won't work.
If you try these classes, please tell me if you found them easy to use and flexible enough. And, of course, please report any bugs you find.
Regards
Christoph