SigSlot Introduction
SigSlot is a library I adopted several years ago as a solid implemention of the "Observer Pattern" in C++.
It's available on GitHub, is free to use, and consists of a single small header file. It supports classical observer patterns with lambda and pointer-to-member connections, and also has support for use as awaitable objects in coroutines.
A little history
Originally, such things were difficult if not impossible to write in C++, and the most common implementations were either non-standard or relied on specialist preprocessors.
The essential concept is that you have a signal object:
sigslot::signal0<> my_signal;
... which other things can listen to:
my_signal.connect(this, &MyListener::slot);
... and the signal can then emit:
my_signal.emit(); // Calls its listeners.
Sarah Thompson wrote the original SigSlot, having decided that the new "ISO C++" standard had sufficient flexibility in the templating language to support the concept - and indeed, it did. She was, as I understand it, explicitly trying to mirror Qt's signal and slot preprocessor.
But ISO C++ - that's C++03, in modern terms - lacked a number of useful features that would make the programmer's life smoother. Sarah's design gave signals parameters - a useful and common feature - but while she could use templates so the type of the parameters didn't matter, she had to use a different template for the different numbers of parameters.
Lambdas didn't exist either, which made for a much more natural fit.
Finally, while Sarah had made the library thread-safe, thread primitives were not covered in C++03, and so several different models needed to be provided for.
Nevertheless, when I came across the library I was pretty impressed with the simplicity - a single header file contains the entire library, and it's simple to use. So I updated things a bit - in no small part in order to learn the new (to me) C++11.
SigSlot C++11
When C++11 was widely available (in 2014), I took Sarah's SigSlot library and updated it with several features from the newer standard.
First, I used "variadic templates", which meant that there was now a single "signal" template no matter what the number or type of arguments the signal took.
Second, I gave it support for lambdas as well as the (somewhat obscure) pointer-to-member syntax.
I didn't do threading, and I later noticed other things could have been improved too. Still, it was handy, and the library shrank a lot. While the library was always a single header file, by using a bit of C++11, I shrank it from 2,500 to just over 500 lines of code.
And Now, C++17...
My last changes have been to introduce features from C++14 and C++17, as well as go over and polish some of the C++11 bits I'd missed.
The first thing to go was the different threading models - I'm now using C++11 thread primitives, and as a result, the last remaining wart in the usage has gone. So, too, did some of the templates - without the different threading models, there was no need for some of the template hierarchy (and if fact I'd missed some simplifications back in 2014).
I also added some coroutine support - this runs on both Windows using MSVC, and UNIXes using CLang. Despite adding about 100 lines of coroutine support, the library is still smaller - just 430 lines now.
Using the library hasn't really changed much since Sarah's version - indeed, this version is closer to hers than to my C++11 version in usage in some ways. In others, though, it's markedly different.
// Only one template, parameters are signal arguments:
sigslot::signal<int> my_signal;
// ...using lambdas, for example:
my_signal.connect(this, [this](int i){
std::cout << "I was signalled with " << i << std::endl;
});
// Emitting is by either emit(...) or simply calling the signal:
my_signal(4);
// Or, do a coroutine:
coroutine_type<int> coroutine() {
int i = co_await my_signal;
co_return i;
}
auto c = coroutine();
// This awaits, so stops immediately.
my_signal.emit(5);
// Now it's continued and is ready to return its value:
std::cout << "I was signalled with " << c.get() << std::endl;
There are more extensive examples in the source tree, including examples for both classical and coroutine based signals which compile clean on both UNIX and Windows.
Why?
Other signal/slot libraries exist - Boost has one, for example. So why do I persist with this one? Partly, it's the small, neat implementation - a design which predates any of my involvement.
But a small library like this is great for me to learn the newer features of C++. It's a fun playground for me, and one that yields useful code. I use SigSlot extensively in my XMPP Thing, Metre.
Feel free to play with it - or use it, even better - it has copyright disclaimed (ie, it's as close to "public domain" as it's possible to get in modern copyright law), and it's available at GitHub.
Top comments (2)
Hi Dave, I would like to use your sigslots C++11 version in a project I'm working on. Looks like only the C++17 version is in your repo. Can you point me to the older, version with no threading? Thanks!
I found what I was looking for in the repo history.