I have, as many of you readers have, seen and/or used other similar libraries. But I have this thing in my head, this crazy notion, that using other people's code is, well, cheating. I can't do it without feeling guilty unless I know that I can implement their ideas on my own. I like walking the hardest path I can for the sake of learning as much as I can. So, when I needed a signals and slots mechanism, I had to write my own library for my own mental health. This is what I came up with.
So what are signals and slots?
Good question. I think it is appropriate to start with an explanation of what they exactly are. Signals and slots are a mechanism for which "events" are handled, passed around, processed, and eventually invoked. Slots connect to signals, and when a signal is fired, it sends data to the referenced slots, allowing that data to be handled arbitrarily. It is important to point out that this referencing of slots to signals is done at run time, allowing for a great deal of flexibility.
Just how lightweight is lightweight?
The short answer: very. In my humble opinion, for a library to be lightweight, it not only needs to provide the smallest subset of useful functionality, but should also feel lightweight. It needs to reflect the "lightweight-ness" in its syntax. To use this library to its full potential requires the use of only two classes and two functions.
Feature-wise, it provides simple and lightweight mechanisms (+1 exception... see below) for attaching, detaching, and invoking slots handling up to 10 parameters.
What it does not do
This library is simple. It does not do the following:
- Parameter binding
- Function retyping
- Reentry protection
- And other "rarely useful" features
Let's talk syntax
First, let's define some functionality where to use the signals and slots system: four functions and a member function.
void print_add(int a, int b)
cout << a << " + " << b << " = " << a + b << endl;
void print_sub(int a, int b)
cout << a << " - " << b << " = " << a - b << endl;
void print_mul(int a, int b)
cout << a << " x " << b << " = " << a * b << endl;
void print_div(int a, int b)
cout << a << " / " << b << " = " << a / (double)b << endl;
void inclass(int a, int b)
cout << "MEMBER: The circumfrence of a " << a
<< " by " << b << " box is " << 2*a + 2*b << endl;
Well, the syntax is simple. Borrowing ideas from C# delegates, connecting these functions and invoking them looks like this:
signal<void, int, int> math_signals;
math_signals += slot(print_add);
math_signals += slot(print_sub);
math_signals += slot(print_mul);
math_signals += slot(print_div);
math_signals += slot(&t, &test::inclass);
The above code adds five slots to the signal, and invokes them with the data 8 and 4. This means that each corresponding function will be executed once, in the order in which they were added, with the parameters 8 4.
Deleting a signal is just as easy. Expanding from the above code, let's say we wanted to remove the third slot, the one pointing to
math_signals -= slot(print_mul);
This snippet will do it.
An alternate using the
() is to use the functions
safeslot are used to create slots to avoid as much explicit template argument declarations. Functions are capable of inferring template arguments, and thus removes much redundant template code as every class and function would have the same signature.
How do you collapse several functions with different return types into one result? This library will only return the result from the last slot fired. This works fine if only one slot is attached per signal. Also, there is no mechanism to marshal the return type of one function into the next. However, there is one trick.
References. Or more specifically, creating signals and slots that take references to data as a parameter. Consider the following code:
void a(int& in)
void b(int& in)
in += 6;
void c(int& in)
in *= 2;
signal<void, int&> cool_test;
cool_test += slot(a);
cool_test += slot(b);
cool_test += slot(c);
int result = 5;
cout << result << endl;
As you probably would expect, the number "24" is printed to screen. Using a referenced parameter allows data to be returned and subsequently modified by the following functions.
Well, I'm sure you probably noticed the one big dangerous shortfall to the code I've presented so far.
If a slot contains a member function pointer, and the pointer to the instance of the class we want to invoke the function in is deleted or goes out of scope, well, things can get nasty quick. If you are lucky, it will still work, but you can end up with a nasty segment fault bringing your ever so wonderful application (without warning, mind you) to its knees.
So, into the spotlight comes the class
trackable. Yes, for those of you familiar with
boost::function, it functions similarly (and yes, a blatant rip-off of the name).
Consider the following example:
using namespace std;
class test_trackable : public semaphore::trackable
void inclass(int a, int b)
cout << "MEMBER: The circumfrence of a " << a << " by "
<< b << " box is " << 2*a + 2*b << endl;
signal<void, int, int> math_signals;
math_signals += safeslot(&track, &test_trackable::inclass);
cout << "TRACKED MEMBER FUNCTION POINTER NOW OUT OF SCOPE!" << endl;
Let's now look at
safeslot. This little function creates a slot which is capable of determining when the instance for the member function has been deleted, and thus avoids a potential disaster. The catch? The class which contains the member function now has to inherit from
semaphore::trackable, which implements a virtual destructor. More on how this mechanism works later.
So, if we where to run this little snippet,
test_trackable::inclass would only be called once - the first time. For compatibilities' sake,
safeslot will also create simple function pointer slots. Any member function that can be wrapped in a safe slot can also be warped in a regular slot (minus the safety).
Let's gut this little fish
Well, I apologize if up to this point this reads a bit like a commercial. But I have to fulfill my responsibility to explain how to use my library, and I wanted to make clear what it can and can't do. So, from here on, I will discuss the design, internal mechanisms, how things fit together, and the problems I encountered.
signal class is just a simple wrapper around a
std::list of slots. Things are kept typesafe with 22 different template specializations to support up to 10 parameters and the
void return type.
slot class is hidden in the
internal namespace. Slots are to be created only through the
safeslot functions provided for the reasons already stated. The
internal::slot class holds a reference counted pointer to an
internal::invokable class which is the workhorse of the slot. Reference counting makes the copy of the class cheap, making storage in the
The invokable, and things that never see the light of the day
There are several worker classes buried in namespaces which are not used directly but do pretty much all the work. They are
internal::invokable and derivatives:
internal::smart_member_function. These classes store function and member function pointers to pieces of code desired to be wrapped in a slot.
internal::simple_function wraps a simple function pointer, and
internal::member_function wraps a member function pointer.
internal::smart_member_function functions similar to the
internal::member_function but adds the ability to be able to determine when the data it's pointed to has expired.
trackable class, as seen above, prevents slots from executing member function pointers after death. The
trackable class, in order for it to tell
internal::smart_member_function that it's been deleted, has to externalize that data. So, it creates an instance of a reference counted watcher class. Any
internal::smart_member_function class created stores its own reference to the watcher class. Once the
trackable class is destroyed, it changes the data of the watcher class, and hence
internal::smart_member_function can discreetly avoid certain disasters. The last
internal::smart_member_function class with the reference to the watcher class will delete it.
Notable complications in design
There was one notable design complication that I feel is worthy enough to deserve its own section: how to compare and equate slots. This functionality is important to be able to delete slots. Previous incarnations of this library failed to deliver this functionality, and left me unable to find a simple method to detach a slot.
Now, in order to see if two slots are the same, we would have to compare their instance of
internal::invokable. Now that said, this base class could not be responsible for this because it's an abstract base class; the important data is held in the classes that derive from it. Furthermore, dynamic_casting is complicated due to the fact that
internal::smart_member_function take one more arbitrary template argument than its base class.
To solve this problem, I added two more virtual functions and an enumeration to
compare(internal::invokable* rhs), and
gettype() returns one of the values from the enum.
compare(...) first checks if the types are the same (and not UserDefined) via
gettype(), and if so, performs a dynamic_casts and a comparison.
Despite first impressions, this is guaranteed to work, and no incorrect casts can be made. In order for the two slots to be comparable, they have to share the same template arguments. Therefore, the
internal::invokable which they hold will also share template arguments. Thus, invoking a compare will result in a dynamic_cast with the correct type and number of arguments. A slot will also allow comparison of incompatible types, obviously returning false in all cases.
There is only one problem with the library that I haven't tackled. As shown, in order to remove a slot, that slot needs to be passed to the disconnect function, like so:
signal<void, int, int> test_signals;
test_signals += safeslot(&t, &test::inclass); test_signals -= safeslot(&t, &test::inclass);
However, if at a later time, a slot needs to be removed, and the class whose slot's function pointer calls is not known, it cannot be removed. I currently am undecided on how to tackle this one, but I'm thinking the most appropriate way would be to use a
NULL to signify a "wildcard" for the class instance, like this:
signal<void, int, int> test_signals;
test_signals += safeslot(&t, &test::inclass); test_signals -= safeslot(NULL, &test::inclass);
Another feature missing that I would like to eventually see included is a control over the order in which the slots are fired.
While this library is simple and lightweight, it still has its flaws. In the end, it has taught me much, and was a fun challenge to complete. I hope someone will find this code useful, and I am looking forward to receiving feedback on my work.