![]() |
Home | Libraries | People | FAQ | More |
The definition of a slot differs amongst signals and slots libraries. Within Boost.Signals, a slot is defined in a very loose manner: it can be any function object that is callable given parameters of the types specified by the signal, and whose return value is convertible to the result type expected by the signal. However, alternative definitions have associated pros and cons that were considered prior to the construction of Boost.Signals.
Slots derive from a specific base
class: generally a scheme such as this will require
all user-defined slots to derive from some library-specified
Slot abstract class that defines a virtual
function calling the slot. Adaptors can be used to convert a
definition such as this to a definition similar to that used
by Boost.Signals, but the use of a large number of small
adaptor classes containing virtual functions has been found to
cause an unacceptable increase in the size of executables
(polymorphic class types require more code than
non-polymorphic types).
This approach does have the benefit of simplicity of implementation and user interface, from an object-oriented perspective.
Slots constructed from a set of
primitives: in this scheme the slot can have a
limited set of types (often derived from a common abstract
base class) that are constructed from some library-defined set
of primitives that often include conversions from free
function pointers and member function pointers, and a limited
set of binding capabilities. Such an approach is reasonably
simple and cover most common cases, but it does not allow a
large degree of flexibility in slot construction. Libraries
for function object composition have become quite advanced and
it is out of the scope of a signals and slots library to
encorporate such enhancements. Thus Boost.Signals does not
include argument binding or function object composition
primitives, but instead provides a hook (via the
visit_each
mechanism) that allows existing binder/composition libraries
to provide the necessary information to Signals.
Users not satisfied with the slot definition choice may opt to replace the default slot function type with an alternative that meets their specific needs.
Users need to have fine control over the connection of
signals to slots and their eventual disconnection. The approach
taken by Boost.Signals is to return a
connection object that enables
connected/disconnected query, manual disconnection, and an
automatic disconnection on destruction mode. Some other possible
interfaces include:
Pass slot to
disconnect: in this interface model, the
disconnection of a slot connected with
sig.connect(slot) is
performed via
sig.disconnect(slot). Internally,
a linear search using slot comparison is performed and the
slot, if found, is removed from the list. Unfortunately,
querying connectedness will generally also end up as
linear-time operations. This model also fails for
implementation reasons when slots become more complex than
simple function pointers, member function pointers and a
limited set of compositions and argument binders: to match the
slot given in the call to
disconnect with an
existing slot we would need to be able to compare arbitrary
function objects, which is not feasible.
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 approach taken by Boost.Signals, 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.
Tokens must be unique, otherwise two slots will have the same name and will be indistinguishable. In environments where many connections will be made dynamically, name generation becomes an additional task for the user. Uniqueness of tokens also results in an additional failure mode when attempting to connect a slot using a token that has already been used.
More parameterization would be required, because the token type must be user-defined. Additional parameterization steepens the learning curver and overcomplicates a simple interface.
This type of interface is supported in Boost.Signals
via the slot grouping mechanism. It augments the
connection object-based
connection management scheme.
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 Signals 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 Signals 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 Signals 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.Signals 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.Signals 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
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 implicit the spec ustar root root
![]() |
Home | Libraries | People | FAQ | More |
The definition of a slot differs amongst signals and slots libraries. Within Boost.Signals, a slot is defined in a very loose manner: it can be any function object that is callable given parameters of the types specified by the signal, and whose return value is convertible to the result type expected by the signal. However, alternative definitions have associated pros and cons that were considered prior to the construction of Boost.Signals.
Slots derive from a specific base
class: generally a scheme such as this will require
all user-defined slots to derive from some library-specified
Slot abstract class that defines a virtual
function calling the slot. Adaptors can be used to convert a
definition such as this to a definition similar to that used
by Boost.Signals, but the use of a large number of small
adaptor classes containing virtual functions has been found to
cause an unacceptable increase in the size of executables
(polymorphic class types require more code than
non-polymorphic types).
This approach does have the benefit of simplicity of implementation and user interface, from an object-oriented perspective.
Slots constructed from a set of
primitives: in this scheme the slot can have a
limited set of types (often derived from a common abstract
base class) that are constructed from some library-defined set
of primitives that often include conversions from free
function pointers and member function pointers, and a limited
set of binding capabilities. Such an approach is reasonably
simple and cover most common cases, but it does not allow a
large degree of flexibility in slot construction. Libraries
for function object composition have become quite advanced and
it is out of the scope of a signals and slots library to
encorporate such enhancements. Thus Boost.Signals does not
include argument binding or function object composition
primitives, but instead provides a hook (via the
visit_each
mechanism) that allows existing binder/composition libraries
to provide the necessary information to Signals.
Users not satisfied with the slot definition choice may opt to replace the default slot function type with an alternative that meets their specific needs.
Users need to have fine control over the connection of
signals to slots and their eventual disconnection. The approach
taken by Boost.Signals is to return a
connection object that enables
connected/disconnected query, manual disconnection, and an
automatic disconnection on destruction mode. Some other possible
interfaces include:
Pass slot to
disconnect: in this interface model, the
disconnection of a slot connected with
sig.connect(slot) is
performed via
sig.disconnect(slot). Internally,
a linear search using slot comparison is performed and the
slot, if found, is removed from the list. Unfortunately,
querying connectedness will generally also end up as
linear-time operations. This model also fails for
implementation reasons when slots become more complex than
simple function pointers, member function pointers and a
limited set of compositions and argument binders: to match the
slot given in the call to
disconnect with an
existing slot we would need to be able to compare arbitrary
function objects, which is not feasible.
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 approach taken by Boost.Si