Boost logo

Boost :

From: Douglas Gregor (gregod_at_[hidden])
Date: 2001-11-21 18:50:33


On Wednesday 21 November 2001 04:48 am, you wrote:
> I'm sorry for possible off-topic, but could you please comment how your
> work is related to libsigc++?

I can give you a comparison 'to the best of my knowledge.' I've looked at the
documentation and implementation of libsigc++ before, but have not used it
extensively.

The libraries are quite similar on the surface - the connection and signal
classes from each have nearly identical interfaces; the SigC::Object class is
quite similar to the "bindable" class from Signals in that a user type
derives from one of these classes to enable automatic connection lifetime
management (which both libraries support).

I'll try to describe the differences in greater detail:

libsigc++ Marshallers vs. Signals Combiners
-----------------------------------------------------
Both of these entities perform the same function: when a signal calls
multiple slots, there must be some way to take the return values of many
slots and make it into a return value for the signal itself.

The libsigc++ Marshaller interface looks like this:
 struct SomeMarshal
    {
       // both typedefs must be defined.
       typedef Type1 InType; // type returned by a slot
       typedef Type2 OutType; // type returned by the signal

       // Return final return code.
       OutType value();
       
       // Return value if marshaller does not get built
       static OutType default_value();

       // Captures return codes and returns TRUE to stop emittion.
       bool marshal(const InType&);

       SomeMarshal();
   };

When a libsigc++ signal invokes its slots, it will essentially do this:
  Marshaller marshaller;
  while (slot = next_slot() && marshaller.marshal(slot(...)));
  return marshaller.value();

The Signals Combiner interface looks like this:
struct Combiner {
  typedef T result_type;
  template<typename InputIterator>
  T operator()(InputIterator first, InputIterator last) const;
};

When a Signals signal invokes its slots, it will do this:
Combiner combiner;
return combiner(first, last);

There is no difference in the power or flexibility afforded by either option.
However, I'll claim that the approach taken by the Signals library is more
natural within the context of C++: the act of calling the slots is merely the
iteration through the input iterator sequence the combiner is given, and the
combiner itself is just a function object taking an input iterator range.
Often, it's much easier to use an interface where you "pull" the data as you
want it: if you don't pull data from a slot, that slot won't be called, so
the equivalent of a libsigc++ marshaller returning "TRUE" from marshal() is a
return statement in a Signals combiner.

It is, of course, a matter of preference, but I believe that a function
object taking an input iterator sequence and returning a value is more "in
the spirit of C++" than defining a special interface for this same operation.

Argument Binding
---------------------
Both libraries "allow" argument binding, and arguments bound in creating
slots can be tracked so that when they die, connections involving them are
removed. The difference between libsigc++ and Signals is mostly
philosophical: libsigc++ includes limited capability for binding arguments,
and can track objects bound in the manner. Signals, on the other hand, does
not do any argument binding. Instead, another binder library (e.g.,
Boost.Bind or Lambda) will perform argument binding and there is a bound
argument discovery interface used by Signals to find those arguments.

This keeps Signals decoupled from binding, so as new binding libraries become
available Signals can adapt to them without any internal changes. libsigc++
doesn't currently have this ability.

What's a Slot?
-----------------
Signals has a much looser definition of a slot than libsigc++ does. libsigc++
is relatively strict regarding what a slot is -- return and argument types
should match exactly with the signal, and if there is a difference one must
manually build an adaptor to the appropriate type.

The Signals definition of a slot is the same as that of a target for
Boost.Function -- anything such that parameters of the type taken by the
signal can be passed to the slot, and the return value of the slot converted
to the type expected by the signal. Thus all adaption is done internally
without user intervention.

I'll claim that the looser definition is more appropriate for a few reasons:
  - It's silly to manually adapt because the signal takes an std::string and
the slot takes a const std::string&.
  - Manual adaptations clutter the code (other side of the coin: they make
conversions explicit -- my side of the coin: they make trivial conversions
explicit, too)
  - Generic libraries define interfaces in terms of expressions, not types.
So saying that we need a function object that takes two ints and returns an
float is uncommon: instead we say that we need the expression f(a, b) to be
convertible to a float when a, b are integers. Signals follows this generic
programming view more closely.

I hope such a comparison was useful, and if there are any other details of
libsigc++ for which information about the Signals equivalent could be
helpful, please do ask.

        Doug


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk