Introduction
This document is an attempt to perform a detailed comparison of two implementations of what is widely known as a Signal/Slot library. The two libraries in question are libsigc++ (v2.x) and Boost.Signals. One of the stated goals of this comparison is to provide some initial analysis as a precursor to submission to the C++ standards body for inclusion in the standard library. libsigc++ was written/maintained by a succession of people including Tero Pulkkinen, Karl Nelson, Murray Cumming and Martin Schulze. Boost.Signals was developed by Douglas Gregor and strongly influenced by the work of Karl Nelson.
In fact, it was Douglas Gregor who asked for a comparison, I am merely the person who picked up the gauntlet [-ed such a pretty little gauntlet, it'll look nice in my collection, too bad it's missing a finger].
This document will be split into two parts. The first being a straightforward comparison of the features of each library and the interface which implements those features.
The second part will be a code comparison of the features of each library, doing an identical task. This will facilitate performance and executable size comparisons. [-ed Just saw Jonathan Brandmeyer's code post, and also Chris Vine's boost example. Will include those shortly.]
In this early draft form, I won't hesitate to provide subjective evaluation from a personal point of view, speaking as a user who (at this point) really hasn't used either of the libraries. I have used the earlier v1.2 version of libsigc++, and I've also used a much earlier library called "Callback" written by Rich Hickey about 8yrs ago. I'll try to mark those views with [--ed(itorial) ] marks, just so everyone's clear which bits can be tossed from the final draft. Discussion's gotta start somewhere.
WARNING: I have not actually compiled any of the code examples. Many were taken straight from the various tutorials and docs from each library. However, there's no guarantee (at this point) that they are correct in any meaningful way.
Features
Both libraries aim to provide Signal/Slot functionality, where a Signal can be loosely described as a publisher, with Slot objects as subscribers. Each library implements this as a one-to-many relationship, with each Signal capable of communicating with an arbitrary number of slots.
The Slot objects are basically functor objects which forward an invocation to the destination function. Each library has the capability to connect to functor objects, class member functions or standalone functions.
Note, for analysis purposes, I'll use the "best" interface for each library, which means Preferred Syntax for Boost.Signals, and the simplified wrapper interfaces for libsigc++. See http://www.boost.org/doc/html/ch06s02.html for more discussion.
[-ed The preferred syntax for Boost.Signals seems quite nice, and I wonder how difficult it would be to add this type of wrapper to the libsigc++ library. ]
Signal Declarations
Both libraries allow signals to be declared for an arbitrary (but not unlimited) number of parameters. Each library also allows the return value type to be specified.
| Boost.Signals | libsigc++ |
|---|---|
|
|
As you can see, the two libraries are very similar when declaring a signal. The main difference being that Boost.Signals allows the use of a complete function signature as the template parameter, instead of listing the return/argument types individually as in libsigc++.
Slot Object
Both libraries implement a Slot object to manage the subscriber relationship and to encapsulate the differences between functor, function pointer, and member function pointer. Boost.Signals implements slots in such a way so that code bloat is minimized and efficiency is maximized (see http://www.boost.org/doc/html/ch06s06.html).
This seems also to be the case with libsigc++ slot objects, which are also templatized. [-ed without looking too hard at the code, the pattern of implementation (i.e. concrete base class with pimpl idiom, derived template classes) seems the same, so I'm guessing they both produce similar bloat factors. Someone correct me??]
When connecting slots to signals, libsigc++ has a more explicit
syntax, requiring the use of sigc::mem_fun() or
sigc::ptr_fun() to construct the appropriate slot object.
[-ed I'm a bit unclear about functor objects, it
seems they can just be passed in the sigc::signal::connect()
function??]
Boost.Signals is able to figure out the proper type of slot to
construct based on the parameters to the
boost::signal::connect() function, as long as it's a
functor object or a ptr-to-function. If it's a member function,
boost::bind() must be used to bind the class object to
the class-member-function ptr.
| Common Code | |
|---|---|
|
|
| Boost.Signals | libsigc++ |
|
|
[-ed I prefer libsigc++ notation of explicitly
using mem_fun() and ptr_fun(), as it seems
to be a bit more consistent in notation. It also nicely follows the
existing C++ standard naming convention for this type of
functionality.]
TODO: answer questions: a) can slots be used independently from signals as functors hiding the actual implementation b) can libsigc++ take plain functor object as parameter to connect()
Slot Binders
Boost.Signals uses the Boost.Bind library, which provides a plethora of useful features. [-ed I'd love this, as long as I didn't have to use all of Boost to get it].
libsigc++ library also provides many features for binding or otherwise transforming function calls. It does so with a few different classes, each of which performs a specific binding task.
A table of equivalent functionality follows:
| Boost.Signals | libsigc++ |
|---|---|
| Boost.Bind | sigc::bind(), sigc::compose(), sigc::hide(), sigc::group() |
| No Equivalent | sigc::bind_return(), sigc::hide_return(), sigc::exception_catch() |
| No Clue | sigc::retype(), sigc::retype_return() |
TODO: answer questions: a) What's up with the No Clue entry, anyone?
Connection Management
Both libraries provide automatic (via derivation) and explicit connection lifetime management. Both libraries also provide a way to check if the connection is valid.
Explicit disconnection
Both libraries provide a method for Slots to be explicitly
disconnected from the signal. Both libraries provide this in the form
of a connection class which is returned when a slot is
connected to the signal. This connection class can be
used to check the status of the connection, and to disconnect the slot
from the signal.
| Common Code | |
|---|---|
|
|
| Boost.Signals | libsigc++ |
|
|
Automatic disconnection
Both libraries provide a method for Slots to be automatically disconnected from the signal when the object the Slot calls is destructed. Both libraries provide this in the form of a helper class to use as a public base class for the connected class in question. Each library even names it similarly.
| Boost.Signals | libsigc++ |
|---|---|
|
|
NOTE: Boost.Signals documentation states that there are exceptions
to using boost::signals::trackable, specifically the
trackable base class is unable to manage connections
created using Boost.Function or Boost.Lambda. This problem is stated
to be resolved in future versions.
Additional Connection Management Features
Additional functionality can be found in the libsigc++ library, in the from of API allowing Slots to be temporarily blocked. See example below:
| libsigc++ |
|---|
|
Advanced Signal Management
There are two special issues associated with the one-to-many signal/slot relation, especially in the context of multiple functions and return values. The first issue is what to do with the return values from multiple Slots, and the second issue is the ordering of the Slot callbacks.
Both libraries provide methods to deal with these issues, but those methods are where we see the first real differences between the library implementations.
Return Values
Both libraries provide ways to manage the return values of the
slots. In fact, the interface of each seems pretty much identical,
minus some cosmetics. In fact, in the following code block, the
aggregate_values struct comes directly from the
Boost.Signals tutorial, but is usable with the libsigc++ library as
well.
| Common Code | |
|---|---|
|
|
| Boost.Signals | libsigc++ |
|
|
Slot Ordering
This is the one area where the two libraries differ significantly.
libsigc++ provides access to the list of slots via an STL-like
slot_list interface, while Boost.Signals hides the
storage implementation and only provides a coarse ordering.
Since libsigc++ provides access to the list of connected slots, it
is a simple matter to insert a Slot anywhere in the list (given an
iterator). In addition, normal push_front() and
push_back() operations are available, as well as forward
and reverse iterators. By default, Slots are pushed onto the end of
the Signal's slot list.
Boost.Signals provides ordering via the Group and GroupCompare
template parameters. Default settings use Group=int and
GroupCompare=std::less<Group>, which by default
implements a simple integer-based ordering scheme. The desired group
can be specified at connect() time, allowing a coarse
ordering of slots. Once a slot is connected, there is no interface
available for reordering slots.
| Common Code | |
|---|---|
|
|
| Boost.Signals | libsigc++ |
|
|
Both libraries will execute the slots in the order in which they were connected [well, to be clear, Boost.Signals will use FIFO ordering in the next release, RSN --Edward Diener].
The Boost.Signals library is capable of disconnecting all signals
of a certain group. This is done via the
boost::signals::signal::disconnect(const group_type&)
function:
| Boost.Signals |
|---|
|
Some developers might like Boost.Signal's handling of slot groups, since it makes it easy to disconnect a subset of slots without having to keep track of the individual connection objects. Similar functionality could be achieved via the libsigc++ library by implementing groups via subsignals. For example:
| libsigc++ |
|---|
|
NOTE: This scheme will not work if the accumulator template parameter is used, since the accumulator changes the effective return value of the master signal, but the subsignals will continue to return only the value of the last slot called. It may be possible to work around this, but that would require two accumulator classes because of the varying return values at each signal level. For example:
| libsigc++ |
|---|
|
As you can see, this gets quite involved, compared to Boost.Signals.
General Notes
Return Value Aggregation
libsigc++ and Boost.Signals both provide a way to combine the return values of multiple slots. Both libraries provide this functionality by way of a template parameter, allowing end developers to provide their own aggregation objects.
The difference is in default values. Boost.Signals provides the
last_value<> template, which iterates through the slot
list and returns the value of the last slot. In comparison, the
default T_accumulator type in libsigc++ signal definition
is 'nil', which triggers template specializations. The libsigc++
documentation states that this is done for efficiency reasons.
[-ed without looking at the code] In preparation for any standardization, I think it would be important to do some real benchmarking to see if the libsigc++ specialization code is really worth the effort in terms of performance.
On the surface, Boost.Signals seems to be the better method, and seems
to be an effective application of the principles shown in
Alexandrescu's "Modern C++ Design" book. In terms of code, it
certainly seems better to defer the specialization to a small
parameterized class than maintain a whole separate Signal
implementation based on <T_accumulator=nil>.
Trackable
Boost.Signals is at a bit of a disadvantage because the
trackable class doesn't work with Boost.Lambda and
Boost.Function. In this bsense, libsigc++ library's implementation
seems superior, if only because you don't have to think about
limitations.
[-ed personal opinion here...
Function Binding
The functionality provided by the Boost.Bind library is equivalent to a subset of the libsigc++ Adaptor classes. However, I like the approach of Boost.Bind, since it subsumes quite a bit of functionality under one interface.
Granted the interface is more subtle in Boost.Bind, whereas libsigc++'s interface to the same feature set is quite explicit. My personal preference is for Boost.Bind, because at least to me it just seems to "do the right thing".
TODO: implement sigc::bind_return(), sigc::hide_return(), sigc::exception_catch() in terms of Boost.Bind. --ed done]
Credits
Special thanks to everyone who read this and contributed. Thanks to Edward Diener for Boost.Signals corrections. Thanks to Murray Cumming for general libsigc++ corrections. Thanks to Jonathan Brandmeyer who implemented the code demonstrations suggested by Murray Cumming. Thanks to Chris Vine who wrote a comprehensive Boost.Signals demo. And of course to Douglas Gregor, who seems to have misplaced his gauntlet.
