First, a common design problem in event-driven programming in C++ will be identified, after which several possible solutions to this problem are examined, listing their advantages and disadvantages. An explanation of the signal/slot mechanism is presented, along with an argumentation on why it is in many cases to be preferred over the alternatives. An implementation of the mechanism, the libsigc++ library, is proposed. Then, a small sample program with the most basic principles of signal/slot programming, using the proposed library, is explained. This program is later expanded on to be useable in a number of other circumstances. Finally, several other use-cases are presented and a conclusion is drawn. Although the presented implementation is cross-platform, emphasis is placed on using it on Microsoft platforms using the Microsoft C++ compiler.
Sample code index
About the sample code
A zipfile with compileable versions of the sample code as presented below is included with this article. Visual Studio 6 and Visual Studio .Net 2002 project files are provided but not all samples work under Visual Studio 6. A trimmed-down version of the libsigc++ library is included but this version should not be used in production systems; rather, the original should be downloaded (the location is listed under references) and installed with the target compiler. It is also necessary to set the library include path to the directory where the contents of the zip file are extracted to.
One of the well-know fundamental aspects of object-oriented programming is known as 'encapsulation'. Encapsulation is the practice of hiding as much of the internal details of an object so as to make it work like a 'black box' as much as possible. Hiding internals makes it easier to make changes to classes later without disrupting the code that is already using them.
In order to achieve as much encapsulation as possible, classes should depend on one another as little as possible. This is especially needed when writing a library because it is impossible for the author of the library to know how and where that library will be used.
But now consider the following situation: there is an object that has to notify another object when a certain event has happend. This could for example be a click on a button, the receiving of data from the network or the end of a long operation such as copying large amounts of data over an ftp connection. The simple solution would be something like the following:
It must be noted that the code above is merely a description of the problem; more specific, in this case a normal return value would have sufficed. Therefore the above problem should be looked at in an asynchronous way, that is to say, as if
Worker::LongOperation() would be spawned into a separate thread and the program would continue execution before
worker.LongOperation() is finished.
It is obvious that this violates the goal of encapsulation. Class Worker has to know about Notifier in order to let Notifier know that the long operation has finished. What would happen when there were other objects that would want to be notified when Worker's
LongOperation() finished? Worker would have to call a function on them, but in order to be able to do that, it would have to have a pointer to them, or know their location another way. That means that Worker would have to be changed: maybe take more arguments in its constructor, or have member functions to get pointers another way.
Worker::LongOperation() would have to be updated: it would have to call member functions on the other objects as well. This is precisely what encapsulation tries to prevent. It is clear that a better solution is needed.
Several solutions to the problem described are possible. It would be outside the scope of this article to explore all of them to the fullest extent, but several of them deserve a description in order to facilitate a comparison between the trade-offs that have to be made when the problem is encountered.
The notifier could ask worker at regular intervals whether it is already finished with its
LongOperation(). This poses several further questions, like how often it should ask. Too much would degrade performance, but waiting too long would lose accuracy. The interval would ideally depend on how long
LongOperation() is expected to take, but what if that cannot be predicted? Another question is what would happen when worker would go away before notifier would notice that it's gone. There would have to be a way for worker to let notifier know that it isn't available any more - all of which would have to be implemented by every class that wants to check for events by worker.
In a Microsoft Windows environment, worker could send notifier what is called a 'message' in Win32-speak, provided notifier has a 'window handle'. This indicates the two biggest problems with this approach: it can only be used for applications for the Microsoft Windows platform, and notifier has to have a window handle, in other words, be a 'window'. This is often not the case, especially in non-GUI code.
A more sophisticated approach would be to let notifier set a function on worker that worker has to call when it finishes its operation. The easiest way would be for notifier to set a pointer to a global function; worker could then store this as a void*. Immediately 2 issues rise: what about the return type and the arguments of the function? And what about type-safety? One of the big advantages of C++ is its deep type safety; that would have to be sacrificed.
The Observer pattern as proposed by Gamma, Helm, Johnson and Vlissides (1995) and described by Patje (2002) and Mariano (2003) seeks to solve the exact problem described above. It is interesting enough to provide a sample implementation in the case described above:
The advantages are obvious: Worker is completely unaware of Notifier, there can be multiple objects that will be notified when
Worker::LongOperation() is finished, and there is full type-safety.
But the encapsulation principle is still broken here since Notifier needs to be aware of Worker. It can only listen to events send by Worker. Also, what when Notifier would have to listen to other events besides those send by Worker? It would have to inherit from a different class for every object listened to. Other problems remain: what when one of the objects in
Worker::m_Observers is deleted? It would have to be deleted from
Worker::m_Observers in order not to let the program go unexpected routes. This would require additional painstaking bookkeeping-code.
None of the solutions presented above can satisfactorily solve the initial problem. Thus another mechanism is introduced: the signal/slot mechanism.
What is signal/slot and why use it?
Signal/slot is a high-level construct to connect certain signals to code that has to be executed when an event takes place, the so-called 'slots'. The three main components of signal/slot programming are clearly put forward in this last sentence: 'signal', 'slot' and 'connect(ion)'. By connecting a signal to a slot, a relationship between the two is made so that when the signal is emitted the slot is called. The prerequisite of this is that a slot is 'callable'. This means that a slot can be a global function, a member method or a static function. The relation between the signal and the slot is the connection; if either the signal or the slot were to go away, the connection would, too, and there would be no need to manually disconnect the signal and the slot. Any signal can have an unlimited number of slots connected to it, and signals can be emitted with any number of arguments They can also get return values from the slots that are connected to them. The way these return values are combined into one is through the use of a marshaller, which will be discussed later.
Signal/slot is largely equivalent to 'delegates' in C# and 'event listeners' in Java.
A C++ implementation of the described signal/slot mechanism is the libsigc++ library. This library was originally written to prove a C++ way of representing events for wrapping up the C-based API of GTK in a C++ library. The first implementation was done by Tero Pulkkinen and later on revised and maintained by Karl Nelson. Nowadays it is maintained as a separate project on its Sourceforge project page by Murray Cumming and Martin Schulze. It is still used as a crucial part of the C++ wrappers around both the GTK and Gnome libraries but it is also widely deployed in other software packages.
The library is heavily template-based but also has a few implementation functions that need to be linked with the program that will use it. At the time of writing (October 31st 2003) version 2.0 of the library is about to be released, but in the rest of the text version 1.2.5 will be used. The 2.0 development branch uses advanced templating that is only understood by the GNU C++ compiler gcc and the Visual Studio .Net 2003 C++ compiler. 1.2.5 is the latest version that can be built using the Visual Studio 6 compiler.
The libsigc++ library is licenced under the LGPL. Contrary to popular belief, this does not mean that it cannot be used in non-GPL applications. It can freely be used in software that is released under any sort of license and you still have access to the full source code and you can make changes to it if you need or want to. One important point however is that if you do decide to make changes to the library itself and those changes are not for your private use, you have to release the source code to those changes. This can be done for example by sending the changes to the development mailing list, but also by putting them on a public website. For details, please see the text of the LGPL.
Building and installing libsigc++ on Windows
Source packages of the library can be downloaded from the project's Sourceforge download page. A readme file for building the library with Visual Studio 6 is included. In short, it comes down to the following steps:
- Download Cygwin from www.cygwin.com/setup.exe and install the default (minimal) setup
- Open a shell, go to /cygdrive/path/to/libsigc/ directory
- Type './configure'.
- Type 'make'.
- Change a registry key to make Visual Studio recognize *.cc files as C++ files. The key is "HKEY_CURRENT_USER\Software\Microsoft\DevStudio\6.0\Build System\Components\Platforms\Win32 (x86)\Tools\32-Bit C/C++-Compiler". There is a field there called 'Input_Spec'. Add ';*.cc' to the value of this string.
Project files for both Visual Studio 6 and Visual Studio .Net 2003 are provided. Compiling the source in Visual Studio will yield one file, independent of whether the Debug or Release build target was chosen: libsigc++1.2-vc6.lib. It is advisable to rename the file in the Debug target directory to libsigc++1.2-vc6D.lib and then copy both files to a place where the linker can find them.
An alternative to creating a static library would be to include the source files into the project that will use it. This is not advisable as it will make a future upgrade and re-use more difficult.
The Visual Studio environment also has to be configured so that it can find the header files that are needed for using the library. This is done by adding the root directory of the source distribution to the include path list.
Using libsigc++: basics
A simple code example will demonstrate the basic usage of the library:
This shows several key points when using libsigc++.
First, a 'signal' is declared in the class that will later on emit that signal. The signal as shown here is the simplest sort of signal; it takes no arguments (the 0 in the name) and it returns void (the template parameter). All libsigc++ classes are in the SigC namespace, so it is imperative that this is specified, unless there is a
#using namespace SigC directive somewhere before the first use of one of the libsigc++ classes. This is a matter of personal taste.
Second, the class of which one of the member functions will be serving as a slot has to be derived from
SigC::Object. As multiple inheritance is allowed in C++, this will seldom pose a problem. The inheritance is needed to get the automatic disconnecting of signals from slots to work. Automatic disconnecting is done when either the slot or the signal disappears. For those cases where it is impossible to derive from a base class (for example when connecting to member functions from class libraries that cannot be changed due to licensing restrictions) the connection management can still be done manually by using
SigC::slot_class() instead of
Thirdly, the signal and the slot have to be connected. This part is the most complicated of the whole process. The signal has a member function 'connect' that has to be called with a '
SigC::slot' parameter. Several utility functions are provided to create objects of this type. The one used here will create a
SigC::slot from an object and a member function of the class that object is based on. Later on other functions to create
SigC::slot objects will be discussed.
Lastly, the signal has to be emitted when the long operation is done. This is done by calling the
emit() member function of the signal. A shortcut is to call the () operator, like so:
Again, this is a matter of personal taste.
Now when the
Worker::LongOperation member function is finished, it will emit the signal, and notifier's slot will be called in response. If notifier were to go away before
Worker::LongOperation was finished, the program would continue running normally. Likewise, if other objects (be they of type Notifier or of another type) were to be interested in the
m_SignalFinished signal, they could just as easily connect a slot to that same signal and all of the slots would be called in response to the emitting of the signal.
Signals with parameters
What if the signal should have one or more parameters? Only very few things would need to be changed, as demonstrated below:
The declaration of the signal has changed, the signature of the slot and the way the signal is emitted.
The declaration of the signal has changed in two ways: to indicate that it takes 1 argument, Signal0 has been changed to Signal1, and a new template parameter has been added to indicate the type of the argument. The changes for making signals with 2 parameters can easily be deducted: it would be named Signal2 and there would be 3 template parameters: the first one for the return type, the second and third one for the types of the arguments. By default, templates for signals with up to 5 parameters are provided. If you need more you will need to modify the m4 source files the headers are generated from; a topic beyond the scope of this text. An example is provided with the libsigc++ source distribution in the file 'examples/nine.h.m4'.
The signature of the slot has also been adapted to take one parameter. Here the type-safety of C++ and the library show: if the change to the slot signature were to be forgotten or if the wrong parameters were to be specified, the compiler would refuse to compile the code. This way correctness of the type of all the arguments is assured.
The last change then is in the way the signal is emitted: the parameter is passed directly into the emit() function. Again, this could have been replaced by the shorthand version as so:
Other types of slots
It is also possible to connect other types of slots to a signal. Say for example that a global function would have to be called when the operation finishes; the code would look as follows:
This is straightforward: pass in the function to be called as a parameter to the
SigC::slot creation function and that function will be called.
To call static functions, the same syntax is used as for the global function, like this:
Slot return values
The first parameter that is always given to the slot<> template is the return type of the slot. So far this has always been void. However it is also possible to write slots with other return types. How can a signal get this value? For signals to which there is only one slot connected, it is very simple.
In case there would have been several slots that were connected to the signal, the result of the last registered one is returned. But what if something more advanced is needed? That is the function of another object, the so-called 'marshaller'. A marshaller is an object that get the results of every subsequent call to a slot, and it can act upon those value. A marshaller can simply gather the results and return them in a
std::vector or it can do calculations on the results (sum, average, any mathematical operation). This is of course assuming the result type is numeric; it could also concatenate strings or do other actions on user defined types.
What does a marshaller look like? The easiest way to illustrate this is by examining an example. The following example will make one long string out of the return values of several slots that each return a string as well.
typedefs at the top of the class declaration are needed to let libsigc++ know about the types of the return value and of the incoming parameters. The value() member function is called to get the final value that will be returned to whoever emitted the signal. The marshal() function then is the function that is called every time a slot has finished and a resulting value is available. It always returns false in the example; when it returns true, will stop signal emission. This can be used for example to stop when a certain value is returned or when a certain threshold has been reached.
Now how is a marshaller used? It is simply passed as a last argument to the declaration of the the signal, like this:
SigC::Signal0<std::string, Concatenator> m_SignalFinished;
Now if this signal is emitted and it has several slots connected to it, the result value will be one string with all the separate result values concatenated to one string.
Instead of the rather abstract approach of two objects 'notifier' and 'worker', a few real-world examples will be provided.
An obvious use would of course be to use it for what libsigc++ was originally developed: connecting events that are generated by a windowing system to handlers for those events; for example, when a button is pressed, call a member function.
(Note that all code in this section is merely pseudo-code and will not compile)
std::cout << "Button pressed!" << std::endl;
int main(int argc, char* argv)
Button btn1 = new Button;
Another use would be to let a Socket object notify other objects when data has arrived on the network:
<do connection setup here>
<read data from socket>
SigC::Signal1<void, std::string> m_DataArrived;
void DataHasArrived(std::string str)
std::cout << "Got data from network:" << str << std::endl;
int main(int argc, char* argv)
Uses for the signal/slot mechanism arise regularly in everyday programming: user interfaces can update themselves when records in a database change, notices can be send (either on-screen or via email) to systems administrators when certain error conditions occurs, ... Many more examples can be thought of.
After examining several design alternatives to implement a callback mechanism the libsigc++ signal/slot library was presented. After introducing the general terminology and implementation principles several day-to-day examples were presented. As with all new and advanced approaches to old problems it takes time to get used to another way of thinking about them, and as with all of those new and advanced approaches not every situation is appropriate to use them. Many are however, so knowing about the idiom and understanding how it works is important for a programmer so that she will recognize the circumstances when it is the best fit.
Other C++ signal/slot implementations:
Thanks to Martin Schulze and Murray Cumming for their comments on early revisions of this text.