ff01014782
[SVN r53569]
406 lines
18 KiB
XML
406 lines
18 KiB
XML
<?xml version="1.0" encoding="utf-8"?>
|
|
<!DOCTYPE section PUBLIC "-//Boost//DTD BoostBook XML V1.0//EN"
|
|
"http://www.boost.org/tools/boostbook/dtd/boostbook.dtd">
|
|
<!--
|
|
Copyright Douglas Gregor 2001-2004
|
|
Copyright Frank Mori Hess 2007-2009
|
|
|
|
Distributed under the Boost Software License, Version 1.0. (See accompanying
|
|
file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
|
-->
|
|
<section last-revision="$Date: 2007-06-12 14:01:23 -0400 (Tue, 12 Jun 2007) $" id="signals2.rationale">
|
|
<title>Design Rationale</title>
|
|
|
|
<using-namespace name="boost::signals2"/>
|
|
<using-namespace name="boost"/>
|
|
<using-class name="boost::signals2::signal"/>
|
|
|
|
<section>
|
|
<title>User-level Connection Management</title>
|
|
|
|
<para> 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
|
|
<code><classname>signals2::connection</classname></code> object that enables
|
|
connected/disconnected query, manual disconnection, and an
|
|
automatic disconnection on destruction mode (<classname>signals2::scoped_connection</classname>).
|
|
In addition, two other interfaces are supported by the
|
|
<methodname alt="signal::disconnect">signal::disconnect</methodname> overloaded method:</para>
|
|
|
|
<itemizedlist>
|
|
<listitem>
|
|
<para><emphasis role="bold">Pass slot to
|
|
disconnect</emphasis>: in this interface model, the
|
|
disconnection of a slot connected with
|
|
<code>sig.<methodname>connect</methodname>(typeof(sig)::slot_type(slot_func))</code> is
|
|
performed via
|
|
<code>sig.<methodname>disconnect</methodname>(slot_func)</code>. 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.</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para><emphasis role="bold">Pass a token to
|
|
disconnect</emphasis>: 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:</para>
|
|
|
|
<itemizedlist>
|
|
<listitem>
|
|
<para>Connections and disconnections must be paired, so
|
|
the problem becomes similar to the problems incurred when
|
|
pairing <code>new</code> and <code>delete</code> for
|
|
dynamic memory allocation. While errors of this sort would
|
|
not be catastrophic for a signals and slots
|
|
implementation, their detection is generally
|
|
nontrivial.</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para>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.</para>
|
|
</listitem>
|
|
</itemizedlist>
|
|
|
|
<para> This type of interface is supported in Boost.Signals2
|
|
via the slot grouping mechanism, and the overload of
|
|
<methodname alt="signal::disconnect">signal::disconnect</methodname>
|
|
which takes an argument of the signal's <code>Group</code> type.</para>
|
|
</listitem>
|
|
</itemizedlist>
|
|
</section>
|
|
|
|
<section>
|
|
<title>Automatic Connection Management</title>
|
|
|
|
<para>Automatic connection management in Signals2
|
|
depends on the use of <classname>boost::shared_ptr</classname> to
|
|
manage the lifetimes of tracked objects. This is differs from
|
|
the original Boost.Signals library, which instead relied on derivation
|
|
from the <code><classname>boost::signals::trackable</classname></code> class.
|
|
The library would be
|
|
notified of an object's destruction by the
|
|
<code><classname>boost::signals::trackable</classname></code> destructor.
|
|
</para>
|
|
<para>Unfortunately, the <code><classname>boost::signals::trackable</classname></code>
|
|
scheme cannot be made thread safe due
|
|
to destructor ordering. The destructor of an class derived from
|
|
<code><classname>boost::signals::trackable</classname></code> will always be
|
|
called before the destructor of the base <code><classname>boost::signals::trackable</classname></code>
|
|
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.
|
|
</para>
|
|
<para>We solve this problem by requiring that tracked objects be
|
|
managed by <classname>shared_ptr</classname>. Slots keep a
|
|
<classname>weak_ptr</classname> to every object the slot depends
|
|
on. Connections to a slot are disconnected when any of its tracked
|
|
<classname>weak_ptr</classname>s expire. Additionally, signals
|
|
create their own temporary <classname>shared_ptr</classname>s to
|
|
all of a slot's tracked objects prior to invoking the slot. This
|
|
insures none of the tracked objects destruct in mid-invocation.
|
|
</para>
|
|
<para>The new connection management scheme has the advantage of being
|
|
non-intrusive. Objects of any type may be tracked using the
|
|
<classname>shared_ptr</classname>/<classname>weak_ptr</classname> scheme. The old
|
|
<code><classname>boost::signals::trackable</classname></code>
|
|
scheme requires the tracked objects to be derived from the <code>trackable</code>
|
|
base class, which is not always practical when interacting
|
|
with classes from 3rd party libraries.
|
|
</para>
|
|
</section>
|
|
|
|
<section>
|
|
<title><code>optional_last_value</code> as the Default Combiner</title>
|
|
<para>
|
|
The default combiner for Boost.Signals2 has changed from the <code>last_value</code>
|
|
combiner used by default in the original Boost.Signals library.
|
|
This is because <code>last_value</code> requires that at least 1 slot be
|
|
connected to the signal when it is invoked (except for the <code>last_value<void></code> 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 <classname>optional_last_value</classname>,
|
|
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
|
|
<classname>boost::optional</classname>.
|
|
</para>
|
|
</section>
|
|
<section>
|
|
<title>Combiner Interface</title>
|
|
|
|
<para> 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.</para>
|
|
|
|
<para> 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.</para>
|
|
|
|
<informaltable>
|
|
<tgroup cols="2" align="left">
|
|
<thead>
|
|
<row>
|
|
<entry><para>Pull</para></entry>
|
|
<entry><para>Push</para></entry>
|
|
</row>
|
|
</thead>
|
|
<tbody>
|
|
<row>
|
|
<entry>
|
|
<programlisting>
|
|
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;
|
|
}
|
|
};
|
|
</programlisting>
|
|
</entry>
|
|
<entry>
|
|
<programlisting>
|
|
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;
|
|
};
|
|
</programlisting>
|
|
</entry>
|
|
</row>
|
|
</tbody>
|
|
</tgroup>
|
|
</informaltable>
|
|
|
|
<para>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 <code>value_type</code>,
|
|
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.</para>
|
|
|
|
<para> 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).</para>
|
|
</section>
|
|
|
|
<section>
|
|
<title>Connection Interfaces: += operator</title>
|
|
|
|
<para> Boost.Signals2 supports a connection syntax with the form
|
|
<code>sig.<methodname>connect</methodname>(slot)</code>, but a
|
|
more terse syntax <code>sig += slot</code> has been suggested (and
|
|
has been used by other signals & slots implementations). There
|
|
are several reasons as to why this syntax has been
|
|
rejected:</para>
|
|
|
|
<itemizedlist>
|
|
<listitem>
|
|
<para><emphasis role="bold">It's unnecessary</emphasis>: the
|
|
connection syntax supplied by Boost.Signals2 is no less
|
|
powerful that that supplied by the <code>+=</code>
|
|
operator. The savings in typing (<code>connect()</code>
|
|
vs. <code>+=</code>) is essentially negligible. Furthermore,
|
|
one could argue that calling <code>connect()</code> is more
|
|
readable than an overload of <code>+=</code>.</para>
|
|
</listitem>
|
|
<listitem>
|
|
<para><emphasis role="bold">Ambiguous return type</emphasis>:
|
|
there is an ambiguity concerning the return value of the
|
|
<code>+=</code> operation: should it be a reference to the
|
|
signal itself, to enable <code>sig += slot1 += slot2</code>,
|
|
or should it return a
|
|
<code><classname>signals2::connection</classname></code> for the
|
|
newly-created signal/slot connection?</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para><emphasis role="bold">Gateway to operators -=,
|
|
+</emphasis>: when one has added a connection operator
|
|
<code>+=</code>, it seems natural to have a disconnection
|
|
operator <code>-=</code>. However, this presents problems when
|
|
the library allows arbitrary function objects to implicitly
|
|
become slots, because slots are no longer comparable. <!--
|
|
(see the discussion on this topic in User-level Connection
|
|
Management). --></para>
|
|
|
|
<para> The second obvious addition when one has
|
|
<code>operator+=</code> would be to add a <code>+</code>
|
|
operator that supports addition of multiple slots, followed by
|
|
assignment to a signal. However, this would require
|
|
implementing <code>+</code> such that it can accept any two
|
|
function objects, which is technically infeasible.</para>
|
|
</listitem>
|
|
</itemizedlist>
|
|
</section>
|
|
<section>
|
|
<title>Signals2 Mutex Classes</title>
|
|
<para>
|
|
The Boost.Signals2 library provides 2 mutex classes: <classname>boost::signals2::mutex</classname>,
|
|
and <classname>boost::signals2::dummy_mutex</classname>. The motivation for providing
|
|
<classname>boost::signals2::mutex</classname> is simply that the <classname>boost::mutex</classname>
|
|
class provided by the Boost.Thread library currently requires linking to libboost_thread.
|
|
The <classname>boost::signals2::mutex</classname> class allows Signals2 to remain
|
|
a header-only library. You may still choose to use <classname>boost::mutex</classname>
|
|
if you wish, by specifying it as the <code>Mutex</code> template type for your signals.
|
|
</para>
|
|
<para>
|
|
The <classname>boost::signals2::dummy_mutex</classname> class is provided to allow
|
|
performance sensitive single-threaded applications to minimize overhead by avoiding unneeded
|
|
mutex locking.
|
|
</para>
|
|
</section>
|
|
<section>
|
|
<title>Comparison with other Signal/Slot implementations</title>
|
|
|
|
<section>
|
|
<title>libsigc++</title>
|
|
|
|
<para> <ulink
|
|
url="http://libsigc.sourceforge.net">libsigc++</ulink> is a C++
|
|
signals & slots library that originally started as part of
|
|
an initiative to wrap the C interfaces to <ulink
|
|
url="http://www.gtk.org">GTK</ulink> 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:</para>
|
|
|
|
<itemizedlist>
|
|
<listitem>
|
|
<para><emphasis role="bold">Slot definitions</emphasis>:
|
|
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).</para>
|
|
</listitem>
|
|
|
|
<listitem>
|
|
<para><emphasis role="bold">Combiner/Marshaller
|
|
interface</emphasis>: 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.</para>
|
|
</listitem>
|
|
</itemizedlist>
|
|
</section>
|
|
|
|
<section>
|
|
<title>.NET delegates</title>
|
|
|
|
<para> <ulink url="http://www.microsoft.com">Microsoft</ulink>
|
|
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:</para>
|
|
|
|
<itemizedlist>
|
|
<listitem>
|
|
<para>Require exact type matches between a delegate and what
|
|
it is calling.</para>
|
|
</listitem>
|
|
|
|
<listitem><para>Only return the result of the last target called, with no option for customization.</para></listitem>
|
|
<listitem>
|
|
<para>Must call a method with <code>this</code> already
|
|
bound.</para>
|
|
</listitem>
|
|
</itemizedlist>
|
|
</section>
|
|
</section>
|
|
</section>
|