这是一篇关于两个著名的C++ signal-slot库 Boost.Signals 和 libsigc++ 的比较文章。
Boost.Signals是著名的Boost库的一部分;libsigc++则被gtk+/gnome的官方C++版本gtkmm/gnomemm所使用。
这篇文章的原始位置在:http://www.3sinc.com/opensource/boost.bind-vs-sigc2.html
Boost.Signals vs libsigc++
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++
// Signal with two args and int return
// preferred usage
boost::signal<int (float, string)> sig;
sig(3.1415926, "pi");
// Signal with two args and int return
// preferred usage
sigc::signal<int, float, string> sig;
sig(3.1415926, "pi");
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
int Func(float val, string str);
class Foo {
public:
// ...
int Func(float val, string str);
};
struct Bar {
int operator()(float val, string str);
};
Foo obj;
Boost.Signals
libsigc++
// Signal with two args and int return
boost::signal<int (float, string)> sig;
sig.connect(Bar());
sig.connect(&Func);
sig.connect(boost::bind(&Foo::Func, obj, _1, _2));
sig(3.1415926, "pi");
// Signal with two args and int return value
sigc::signal<int, float, string> sig;
sig.connect(Bar());
sig.connect(sigc::ptr_fun(&Func));
sig.connect(sigc::mem_fun(obj, &Foo::Func));
sig(3.1415926, "pi");
[-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
int Func1(float val, string str);
int Func2(float val, string str);
Boost.Signals
libsigc++
// Signal with two args and int return
boost::signal<int (float, string)> sig;
boost::signals::connection c =
sig.connect(&Func1);
sig.connect(&Func2);
// Func1(), then Func2()
sig(3.1415926, "pi");
c.disconnect();
// only Func2()
sig(1.414141414, "sqrt(2)");
assert(!c.connected());
// Signal with two args and int return value
sigc::signal<int, float, string> sig;
sigc::connection c =
sig.connect(sigc::ptr_fun(&Func1));
sig.connect(sigc::ptr_fun(&Func2));
// Func1(), then Func2()
sig.emit(3.1415926, "pi");
c.disconnect();
// only Func2()
sig(1.414141414, "sqrt(2)");
assert(!c.connected());
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++
struct Functee :
public boost::signals::trackable {
int operator()(float val, string str);
};
Functee* f = new Functee();
Functee* g = new Functee();
// Signal with two args and int return
boost::signal<int (float, string)> sig;
sig.connect(*f);
sig.connect(*g);
// f::operator()(), then g::operator()()
sig(3.1415926, "pi");
delete g;
// only f::operator()()
sig(1.414141414, "sqrt(2)");
struct Functee :
public sigc::trackable {
int operator()(float val, string str);
};
Functee* f = new Functee();
Functee* g = new Functee();
// Signal with two args and int return
sigc::signal<int, float, string> sig;
sig.connect(sigc::ptr_fun(f, &Functee::operator()));
sig.connect(sigc::ptr_fun(g, &Functee::operator()));
// f::operator()(), then g::operator()()
sig.emit(3.1415926, "pi");
delete g;
// only f::operator()()
sig(1.414141414, "sqrt(2)");
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++
int Func1(float val, string str);
int Func2(float val, string str);
// Signal with two args and int return value
sigc::signal<int, float, string> sig;
sigc::connection c = sig.connect(sigc::ptr_fun(&Func1));
sig.connect(sigc::ptr_fun(&Func2));
sig.emit(3.1415926, "pi"); // Func1(), then Func2()
c.block(); // temporarily blocked
sig(1.414141414, "sqrt(2)"); // only Func2()
c.unblock(); // back to normal
sig.emit(3.1415926, "pi"); // Func1(), then Func2()
c.disconnect();
sig(1.414141414, "sqrt(2)"); // only Func2()
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
int Func1(float val, string str);
int Func2(float val, string str);
template<typename Container>
struct aggregate_values
{
typedef Container result_type;
template
Container operator()(InputIterator first, InputIterator last) const
{
return Container(first, last);
}
};
Boost.Signals
libsigc++
// Signal with two args and int return value
boost::signal<int( float, string),
aggregate_values<std::vector<int> > > sig;
sig.connect(&Func1);
sig.connect(&Func2);
// Func1(), then Func2()
std::vector<int> res = sig(3.1415926, "pi");
// Signal with two args and int return value
sigc::signal<int, float, string,
aggregate_values<std::vector<int> > > sig;
sig.connect(sigc::ptr_fun(&Func1));
sig.connect(sigc::ptr_fun(&Func2));
// Func1(), then Func2()
std::vector<int> res = sig(3.1415926, "pi");
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
int Func1(float val, string str);
int Func2(float val, string str);
Boost.Signals
libsigc++
// Signal with two args and int return value
boost::signal<int (float, string)> sig;
sig.connect(1, &Func1);
sig.connect(0, &Func2);
sig(3.1415926, "pi"); // Func2(), then Func1()
// Signal with two args and int return value
sigc::signal<int, float, string> sig;
sig.connect(sigc::ptr_fun(&Func1));
sig.slots().push_front(sigc::ptr_fun(&Func2));
sig(3.1415926, "pi"); // Func2(), then Func1()
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
int Func1(float val, string str);
int Func2(float val, string str);
// Signal with two args and int return value
boost::signal sig;
boost::signals::connection c = sig.connect(1, &Func1);
sig.connect(0, &Func2);
sig(3.1415926, "pi"); // Func2(), then Func1()
sig.disconnect(1);
sig(1.414141414, "sqrt(2)"); // only Func2()
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++
int Func1(float val, string str);
int Func2(float val, string str);
typedef sigc::signal<int, float, string> MySig;
// Signal with two args and int return value
MySig sig;
MySig* group1 = new MySig();
MySig* group2 = new MySig();
sig.connect(group1->make_slot());
sig.connect(group2->make_slot());
group1->connect(sigc::ptr_fun(&Func1));
group2->connect(sigc::ptr_fun(&Func2));
std::vector<int> res = sig(3.1415926, "pi"); // Func1(), then Func2()
delete group1;
std::vector<int> res = sig(3.1415926, "pi"); // just Func2()
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++
template<typename Container>
struct aggregate_values
{
typedef Container result_type;
template
Container operator()(InputIterator first, InputIterator last) const
{
return Container(first, last);
}
};
template<typename Container>
struct aggregate_aggregates
{
typedef Container result_type;
template
Container operator()(InputIterator first, InputIterator last) const
{
Container result;
for(InputIterator it = first; it != last; ++it){
copy(it->begin(), it->end(), back_inserter(result));
}
return result;
}
};
typedef aggregate_aggregates<std::vector<int> > MyAgAg;
typedef aggregate_values<std::vector<int> > MyAgVal;
typedef sigc::signal<MyAgVal::result_type, float, string, MyAgAg> MySig;
typedef sigc::signal<int, float, string, MyAgVal> MySubSig;
// Signal with two args and int return value
MySig sig;
MySig* group1 = new MySubSig();
MySig* group2 = new MySubSig();
sig.connect(group1->make_slot());
sig.connect(group2->make_slot());
group1->connect(sigc::ptr_fun(&Func1));
group2->connect(sigc::ptr_fun(&Func2));
std::vector<int> res = sig(3.1415926, "pi"); // Func1(), then Func2()
delete group1;
std::vector<int> res = sig(3.1415926, "pi"); // just Func2()
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.