Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

A Minimalistic Signals and Slots Implementation

4.60/5 (5 votes)
8 Aug 20068 min read 1   351  
A lightweight and typesafe templated signals and slots implementation.

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.

Image 1

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.

Image 2

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);    // Add slot
// ...
test_signals -= safeslot(&t, &test::inclass);    // Remove slot

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);    // Add slot
// ...
test_signals -= safeslot(NULL, &test::inclass);    // Remove slot

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here