Introduction
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;
}
class test
{
public:
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:
test t;
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);
math_signals(8, 4);
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 print_mul
.
math_signals -= slot(print_mul);
This snippet will do it.
An alternate using the +=
, -=
, and ()
is to use the functions connect
, disconnect
, and emit
, respectively.
Functions slot
and 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.
Return types
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)
{
++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;
cool_test(result);
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.
+1 exception
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
{
public:
void inclass(int a, int b)
{
cout << "MEMBER: The circumfrence of a " << a << " by "
<< b << " box is " << 2*a + 2*b << endl;
}
};
int main()
{
signal<void, int, int> math_signals;
{
test_trackable track;
math_signals += safeslot(&track, &test_trackable::inclass);
math_signals(8, 4);
}
cout << "TRACKED MEMBER FUNCTION POINTER NOW OUT OF SCOPE!" << endl;
math_signals(8, 4);
system("pause");
return 0;
};
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.
The signal
The 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.
The slot
The slot
class is hidden in the internal
namespace. Slots are to be created only through the slot
and 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 signal
class' std::list
efficient.
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::simple_function
, internal::member_function
, and 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.
The trackable
The 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::member_function
and 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 internal::invokable
: gettype()
, compare(internal::invokable* rhs)
, and
enum type
{
SimpleFunction,
MemberFunction,
SmartMemberFunction,
UserDefined
};
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.
Problems
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 t;
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 t;
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.
Conclusion
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.