|  | Home | Libraries | People | FAQ | More | 
 Users need to have fine control over the connection of
    signals to slots and their eventual disconnection. The primary approach
    taken by Boost.Signals2 is to return a
    signals2::connection object that enables
    connected/disconnected query, manual disconnection, and an
    automatic disconnection on destruction mode (signals2::scoped_connection).
    In addition, two other interfaces are supported by the
    signal::disconnect overloaded method:
Pass slot to
        disconnect: in this interface model, the
        disconnection of a slot connected with
        sig.connect(typeof(sig)::slot_type(slot_func)) is
        performed via
        sig.disconnect(slot_func). Internally,
        a linear search using slot comparison is performed and the
        slot, if found, is removed from the list. Unfortunately,
        querying connectedness ends up as a
        linear-time operation.
Pass a token to disconnect: this approach identifies slots with a token that is easily comparable (e.g., a string), enabling slots to be arbitrary function objects. While this approach is essentially equivalent to the connection approach taken by Boost.Signals2, it is possibly more error-prone for several reasons:
Connections and disconnections must be paired, so
            the problem becomes similar to the problems incurred when
            pairing new and delete for
            dynamic memory allocation. While errors of this sort would
            not be catastrophic for a signals and slots
            implementation, their detection is generally
            nontrivial.
If tokens are not unique, two slots may have the same name and be indistinguishable. In environments where many connections will be made dynamically, name generation becomes an additional task for the user.
 This type of interface is supported in Boost.Signals2
        via the slot grouping mechanism, and the overload of
        signal::disconnect
        which takes an argument of the signal's Group type.
Automatic connection management in Signals2
      depends on the use of boost::shared_ptr to
      manage the lifetimes of tracked objects.  This is differs from
      the original Boost.Signals library, which instead relied on derivation
      from the boost::signals::trackable class.
      The library would be
      notified of an object's destruction by the
      boost::signals::trackable destructor.
    
Unfortunately, the boost::signals::trackable
      scheme cannot be made thread safe due
      to destructor ordering.  The destructor of an class derived from
      boost::signals::trackable will always be
      called before the destructor of the base boost::signals::trackable
      class.  However, for thread-safety the connection between the signal and object
      needs to be disconnected before the object runs its destructors.
      Otherwise, if an object being destroyed
      in one thread is connected to a signal concurrently
      invoking in another thread, the signal may call into
      a partially destroyed object.
    
We solve this problem by requiring that tracked objects be
      managed by shared_ptr.  Slots keep a
      weak_ptr to every object the slot depends
      on.  Connections to a slot are disconnected when any of its tracked
      weak_ptrs expire.  Additionally, signals
      create their own temporary shared_ptrs to
      all of a slot's tracked objects prior to invoking the slot.  This
      insures none of the tracked objects destruct in mid-invocation.
    
The new connection management scheme has the advantage of being
      non-intrusive.  Objects of any type may be tracked using the
      shared_ptr/weak_ptr scheme.  The old
      boost::signals::trackable
      scheme requires the tracked objects to be derived from the trackable
      base class, which is not always practical when interacting
      with classes from 3rd party libraries.
    
      The default combiner for Boost.Signals2 has changed from the last_value
      combiner used by default in the original Boost.Signals library.
      This is because last_value requires that at least 1 slot be
      connected to the signal when it is invoked (except for the last_value<void> specialization).
      In a multi-threaded environment where signal invocations and slot connections
      and disconnections may be happening concurrently, it is difficult
      to fulfill this requirement.  When using optional_last_value,
      there is no requirement for slots to be connected when a signal
      is invoked, since in that case the combiner may simply return an empty
      boost::optional.
    
The Combiner interface was chosen to mimic a call to an algorithm in the C++ standard library. It is felt that by viewing slot call results as merely a sequence of values accessed by input iterators, the combiner interface would be most natural to a proficient C++ programmer. Competing interface design generally required the combiners to be constructed to conform to an interface that would be customized for (and limited to) the Signals2 library. While these interfaces are generally enable more straighforward implementation of the signals & slots libraries, the combiners are unfortunately not reusable (either in other signals & slots libraries or within other generic algorithms), and the learning curve is steepened slightly to learn the specific combiner interface.
The Signals2 formulation of combiners is based on the combiner using the "pull" mode of communication, instead of the more complex "push" mechanism. With a "pull" mechanism, the combiner's state can be kept on the stack and in the program counter, because whenever new data is required (i.e., calling the next slot to retrieve its return value), there is a simple interface to retrieve that data immediately and without returning from the combiner's code. Contrast this with the "push" mechanism, where the combiner must keep all state in class members because the combiner's routines will be invoked for each signal called. Compare, for example, a combiner that returns the maximum element from calling the slots. If the maximum element ever exceeds 100, no more slots are to be called.
| Pull | Push | 
|---|---|
| 
struct pull_max {
  typedef int result_type;
  template<typename InputIterator>
  result_type operator()(InputIterator first,
                         InputIterator last)
  {
    if (first == last)
      throw std::runtime_error("Empty!");
    int max_value = *first++;
    while(first != last && *first <= 100) {
      if (*first > max_value)
        max_value = *first;
      ++first;
    }
    return max_value;
  }
};
 | 
struct push_max {
  typedef int result_type;
  push_max() : max_value(), got_first(false) {}
  // returns false when we want to stop
  bool operator()(int result) {
    if (result > 100)
      return false;
    if (!got_first) {
      got_first = true;
      max_value = result;
      return true;
    }
    if (result > max_value)
      max_value = result;
    return true;
  }
  int get_value() const
  {
    if (!got_first)
      throw std::runtime_error("Empty!");
    return max_value;
  }
private:
  int  max_value;
  bool got_first;
};
 | 
There are several points to note in these examples. The
    "pull" version is a reusable function object that is based on an
    input iterator sequence with an integer value_type,
    and is very straightforward in design. The "push" model, on the
    other hand, relies on an interface specific to the caller and is
    not generally reusable. It also requires extra state values to
    determine, for instance, if any elements have been
    received. Though code quality and ease-of-use is generally
    subjective, the "pull" model is clearly shorter and more reusable
    and will often be construed as easier to write and understand,
    even outside the context of a signals & slots library.
The cost of the "pull" combiner interface is paid in the implementation of the Signals2 library itself. To correctly handle slot disconnections during calls (e.g., when the dereference operator is invoked), one must construct the iterator to skip over disconnected slots. Additionally, the iterator must carry with it the set of arguments to pass to each slot (although a reference to a structure containing those arguments suffices), and must cache the result of calling the slot so that multiple dereferences don't result in multiple calls. This apparently requires a large degree of overhead, though if one considers the entire process of invoking slots one sees that the overhead is nearly equivalent to that in the "push" model, but we have inverted the control structures to make iteration and dereference complex (instead of making combiner state-finding complex).
 Boost.Signals2 supports a connection syntax with the form
    sig.connect(slot), but a
    more terse syntax sig += slot has been suggested (and
    has been used by other signals & slots implementations). There
    are several reasons as to why this syntax has been
    rejected:
It's unnecessary: the
        connection syntax supplied by Boost.Signals2 is no less
        powerful that that supplied by the +=
        operator. The savings in typing (connect()
        vs. +=) is essentially negligible. Furthermore,
        one could argue that calling connect() is more
        readable than an overload of +=.
Ambiguous return type:
        there is an ambiguity concerning the return value of the
        += operation: should it be a reference to the
        signal itself, to enable sig += slot1 += slot2,
        or should it return a
        signals2::connection for the
        newly-created signal/slot connection?
Gateway to operators -=,
        +: when one has added a connection operator
        +=, it seems natural to have a disconnection
        operator -=. However, this presents problems when
        the library allows arbitrary function objects to implicitly
        become slots, because slots are no longer comparable.  
 The second obvious addition when one has
        operator+= would be to add a +
        operator that supports addition of multiple slots, followed by
        assignment to a signal. However, this would require
        implementing + such that it can accept any two
        function objects, which is technically infeasible.
      The Boost.Signals2 library provides 2 mutex classes: boost::signals2::mutex,
      and boost::signals2::dummy_mutex.  The motivation for providing
      boost::signals2::mutex is simply that the boost::mutex
      class provided by the Boost.Thread library currently requires linking to libboost_thread.
      The boost::signals2::mutex class allows Signals2 to remain
      a header-only library.  You may still choose to use boost::mutex
      if you wish, by specifying it as the Mutex template type for your signals.
    
      The boost::signals2::dummy_mutex class is provided to allow
      performance sensitive single-threaded applications to minimize overhead by avoiding unneeded
      mutex locking.
    
libsigc++ is a C++ signals & slots library that originally started as part of an initiative to wrap the C interfaces to GTK libraries in C++, and has grown to be a separate library maintained by Karl Nelson. There are many similarities between libsigc++ and Boost.Signals2, and indeed the original Boost.Signals was strongly influenced by Karl Nelson and libsigc++. A cursory inspection of each library will find a similar syntax for the construction of signals and in the use of connections. There are some major differences in design that separate these libraries:
Slot definitions: slots in libsigc++ are created using a set of primitives defined by the library. These primitives allow binding of objects (as part of the library), explicit adaptation from the argument and return types of the signal to the argument and return types of the slot (libsigc++ is, by default, more strict about types than Boost.Signals2).
Combiner/Marshaller interface: the equivalent to Boost.Signals2 combiners in libsigc++ are the marshallers. Marshallers are similar to the "push" interface described in Combiner Interface, and a proper treatment of the topic is given there.
Microsoft has introduced the .NET Framework and an associated set of languages and language extensions, one of which is the delegate. Delegates are similar to signals and slots, but they are more limited than most C++ signals and slots implementations in that they:
Require exact type matches between a delegate and what it is calling.
Only return the result of the last target called, with no option for customization.
Must call a method with this already
          bound.
| Last revised: June 12, 2007 at 14:01:23 -0400 |