Boost logo

Boost :

From: Douglas Gregor (gregod_at_[hidden])
Date: 2001-12-27 11:00:31


On Thursday 27 December 2001 01:30 am, you wrote:
> Because some the chief arguments seems to be included in this
> library being proposed. The use of numbers after the signal
> names was one of the key complaints cited when I proposed sigc++
> to boost. Obviously these arguments are dropped some place!?

Not dropped at all. The numbered variants exist for binary compatibility and
to allow users to reduce the amount of code that needs to be compiled (e.g.,
by including only the header for a signal/function with 2 arguments). The
unnumbered classes still exist, and only day the committee may decide that
template typedefs should be added to the language and then the binary
compatibility argument will no longer hold. For now, the best we can do is
have signal (or function) derive the appropriate signalN (or functionN) class
and reimplement some functionality that won't be found because of lookup
rules.

> > This, for me, is the main reason I believe that SigC++ does not belong in
> > Boost. Why should a signals & slots library contain code for binding
> > arguments in calls to member functions? Why should it contain adaptors to
> > perform type conversions on slot call arguments when the types don't
> > match exactly? There are libraries that do this type of operation - Bind,
> > Lambda, FACT!, FC++, Spirit semantic actions, etc. - and do it better
> > than we can expect a signals & slots library to do it, because a signals
> > & slots library isn't about argument binding or function composition at
> > all.
>
> Because as always stated, tracking of elements must run through the
> adaptors. The signal code proposed does not address this as far
> as I can tell.

It does. The visit_each free function applies a visitor to all bound objects
within a function object. Boost.Bind implements the visit_each functionality,
and other libraries can be adapted to do the same. There are many other
reasons one may want the ability to visit all subobjects (e.g., garbage
collection or persistence), so I would expect that "visit all subobjects"
functionality will become more common; why restrict such a useful thing to a
signals & slots library?

> If you construct a functor from a trackable element
> using any of those other libraries you name, you lose the ablity to
> track the object.

If they do not implement visit_each, yes.

> Any good signal/slot library will need to be
> integrated with the binding of objects/methods and callbacks.
> Doing it any other way greatly increases the code bloat.

Not necessarily. There need be no difference between the sizes of the
following when the Callback type is unchanged:

template<typename R, typename Callback = Default> class signal0;
template<typename R> class signal0;

The overall program size only changes when signal0<R, my_callback> and
signal0<R, her_callback> have much of the same code that doesn't depend on
the callback type. The solution is generally to factor out the common
functionality into a base class. There are more radical solutions. For
instance, here is is an example of the Signals library:

The Signals library supports named slots, and the type of the name is
user-configurable. The signal0 definition looks something like:

template<typename R, typename SlotName = std::string,
typename SlotNameCompare = std::less<SlotName> >
class signal0;

Internally, the slot names are stored in a std::map, so this can result in
massive code bloat when a user changes the SlotName parameter (a new std::map
is instantiated, and those are huge). This can still be factored using a form
of type erasure. The "signal_base" class actually contains a named slot
mapping that looks like this:

std::map<boost::any, boost::connection,
         boost::function2<bool, boost::any, boost::any> > named_slots;

So we have one instantiation of an std::map for the entire signals & slots
library, regardless of the slot name type or comparison function object type.
Introducing a new slot name type or comparison function object type requires
only a few small instantiations.

This notion is easily extended, and one could create an entire "mini-STL"
that "acts like" the STL containers and algorithms, but all of the underlying
code is non-template code based on boost::any and boost::function.

> And worse overly complicates the code.

Absolutely not. The code becomes simpler because the usage of, for instance,
a callback type is trivial. There are only two major operations on the
callback type:
  - construct from a slot/function object
  - call with a given set of arguments

The difference is that a "connect" method might look like:
template<typename Slot> void connect(const Slot& slot);

instead of:

void connect(const slot_type& slot);

> Growing each concept separately, often creates a large set of not
> entirely integrated peices which in turn lead to horrible bloat and
> inefficency.

If the underlying concepts are poorly specified, then perhaps bloat and
inefficiency would be the result. Callback and function object concepts are
quite trivial, and have been used efficiently for a quite a while. Why would
they fail here?

> I think your goal is not exactly what should be that of boost. Is it
> the point of a standards orginization to simply hand down standards which
> someone arbitarily feels is best or should it be adapted from those
> items which are found in industry to already work well?

Existing practice is an important thing for sure, but standards bodies should
also be forward-thinking: will the restrictions of the "existing practice"
limit users in the future? What's the point of having a great binding library
if you can't use it with signals & slots?

> I am not saying
> fresh code is not needed, but you seem to really be reinventing the wheel.
> Considering most of the interface (signals method name, connection
> concept) were taken from sigc++ why not adapt the whole thing rahter
> than such peice meal.

I'm trying to generalize the wheel - take away the reliance on concrete
classes and specific implementation behavior, and look at signals & slots
from the abstract point of view. I looked at what was available - SigC++, Qt,
.NET delegates, and a few other libraries - and concluded that abstracting
the existing code would require much more work that starting anew. Other
benefits of starting from a new code base include the ability to be _very_
careful with regard to exceptions, because signals & slots systems are very
fragile in the face of exceptions. I wasn't able to convince myself that any
of the C++ libraries out there had been designed with exception safety in
mind.

If an exception is thrown while connecting a signal to a slot, will the
signal and slot be left in a working state? Say I'm connecting a signal "sig"
to a slot "slot", where "slot" contains two trackable objects, and in trying
to allocate memory to store the connection object for the second trackable
object an exception is thrown. Then we have the first trackable object
thinking that it is connected to the signal, but the second isn't; perhaps
"sig" thinks its connected to the slot and perhaps not. In any case, our
connection is not complete and either the "sig" object or the first trackable
object is left in an inconsistent state. Has this been considered for SigC++,
because it isn't documented AFAICT?

> > Orthogonality in library design allows rapid innovation.
>
> I don't see the other one as anything other than different implemention
> without some of the major features. Tracking of dependencies, ablity
> to adapt while maintaining those and a slot concept which can be
> grown are all part of sigc++. It seems a fairly orthagonal set.

It's an orthogonal set conceptually, yes, so why is it in a single library?
By orthogonal I mean that each piece of the puzzle is a separate entity that
performs a single, well-defined task. The pieces can be assembled to create a
unified signals & slots system. If one piece breaks, or some genius comes
along with a better way to perform one of those tasks, we can just swap out
the old piece and replace it with the new piece.

Consider, for the purpose of analogy, computer hardware. We have two options:
a mainboard with an AGP slot and a mainboard with integrated video chip. I
can upgrade the video card by buying the latest-and-greatest from Matrix, or
ATI, or any other manufacturer whose products conform to the AGP bus
specifications. Or, I can update my mainboard with a new offering from Tyan,
or Asss, or any other manufacturer whose products conform to the AGP bus
specifications. Meanwhile, in my other computer the mainboard and video are
completely tied: ATI can make the most amazing advances in video technology,
but I cannot use this until it makes its way to the proprietary
mainboard-to-video-chip interface AND manufacturers make a new mainboard that
contains that graphics chip. If it isn't obvious, Signals is the mainboard,
Bind is a video card, and AGP is the visitable function object concept.
Amazing advances in video card technology give us the Lambda Ti500, and
Signals is ready for it.

> Lack of adaptors. SigC++ provides....
[I'm reordering and grouping these]

> bind_return - give a return value to a void slot.

This could be a useful addition to <functional> - why is it in a signals &
slots library, when it can be used with any algorithm or data structure that
uses function objects?

> bind - place a value onto the last argument of a slot.
> chain - call one slot with the return of another.
> hide - add an used parameter to a slot.
> convert - add intermedate function to a slot.
> slot (methods, object+method, function)
> - create unified slot type to bind all those elements
> with tracking.
> closure (obj in slot+method)
> - functor slot.

Boost.Bind can do all of these, with a unified syntax, as can several other
binding/composition libraries. What if I want to trap a type of exception? Or
use operator notation on my slots?

> class_slot (object+method) - slot without tracking.
> auto_disconnect - disconnect after a specific number of calls.

These aren't covered by any binding library, because they are the result of
merging the argument binding and signals & slots libraries. They can easily
be added: class_slot is just a function object adaptor for which visit_each
is specialized to do nothing, and auto_disconnect can be implemented as an
adaptor containing a call count and a connection.

> Providing this set with the boost::signal implementation or trying to
> pull them in piecemeal will give poor results. SigC++ draws rave
> reviews from its users who frequently tell me it provides everything
> they need so much so that when they discover "gee it would be nice to
> have this" most I the time I point out it is already there.

And we'll point out how it can be done with a Boost library. That knowledge
will carry on to other tasks that have nothing to do with signals & slots.
Say I've learned how to chain/compose slots in SigC++ - does this help me
with function composition when calling std::transform? If I've learned
chaining/composition using the Boost Bind or Compose libraries, I can
immediately apply that knowledge to Signals and to std::transform.

Would you argue that std::vector should have a sort() method, so that it is
more complete?

> The reason is you are implementing the same system which I went
> away from when I converted Tero's code. That is requiring the
> Signal to know something about the Slot.
>
> signal.connect(functor())
>
> implies that functor has operator()() of the type used by signal
> and the signal gives it this info.
>
> Now lets try to place an adaptor in the path...
>
> signal.connect(adaptor(functor());
>
> adaptor must implement an operator()() for a connection type
> which is not known.

visit_each solves this. The adaptor need not know anything about the
connection.

> Thus sigc++ is really more orthagonal as
> the tools were meant to work together.

Signals is meant to work with Bind, with Function, and other libraries as
well. std::sort is meant to work with std::vector, but <algorithm> doesn't
include <vector> and <vector> need not include <algorithm>. They're loosely
coupled through the RandomAccessIterator concept, and they work very well
together. So long as Bind fulfills its requirements (by generating a
CopyConstructible and Visitable function object), and Function fulfills its
requirements (by constructing itself from a CopyConstructible function object
and being able to call that function object through operator()), the Signals
library will work well with both of them. So will Compose, or <functional>,
or SigC++ argument binding, or function pointers...

> > Suggestions always welcome. Signals & slots seems to be the "popular"
> > adopted name, and thus I chose to keep it even though slots are not
> > concrete entities .
>
> I considered dropping signal name myself, but as I have an actual slot
> concept it makes sense. Implementing signal by itself (without the slot)
> just makes it too confusing. It should be a function_list or something
> like that.

Signals has a slot concept.

> The concept of signal/slot is simply any type of function/functor/method/
> closure goes into a unified slot interface and that is what signal accepts.

... and the only difference in the unified slot interface is that SigC++ has
the word "slot" but Signals doesn't? I fail to see the conceptual difference
between sig.connect(slot(function_obj)); and sig.connect(function_obj).

        Doug


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