This document describes the high-level design of the Boost.Signals library. An additional document describes the design rationale.
Type erasure is used extensively within the Boost.Signals library
to reduce the amount of code generated by template
instantiation. Each signal must manage a list of slots and their
associated connections, along with a std::map
to map from
slot names to their associated connections. However, instantiating
this map for every token type, and perhaps within each translation
unit (for some popular template instantiation strategies) increase
compile time overhead and space overhead.
To combat this so-called "template bloat", we use Boost.Function and Boost.Any to store unknown types and
operations. Then, all of the code for handling the list of slots and
the mapping from named slots to connections is factored into the class
signal_base
that deals exclusively with the any
and
function
objects, hiding the actual implementations using
the well-known pimpl idiom. The actual signalN
class
templates deal only with code that will change depending on the number
of arguments or which is inherently template-dependent (such as
connection).
connection
class The connection
class is central to the behavior of
the Boost.Signals library. It is the only entity within the
Boost.Signals system that has knowledge of all objects that are
associated by a given connection. To be specific, the
connection
class itself is merely a thin wrapper over a
shared_ptr
to a basic_connection
object.
connection
objects are stored by all participants in
the Signals system: each trackable
object
contains a list of connection
objects describing all
connections it is a part of; similarly, all signals contain a set of
pairs that define a slot. The pairs consist of a slot function object
(generally a Boost.Function
object) and a connection
object (that will disconnect on
destruction). Finally, the mapping from slot groups to slots is based
on the key value in a std::multimap
(the stored data in
the std::multimap
is the slot pair).
The slot call iterator is conceptually a stack of iterator adaptors that modify the behavior of the underlying iterator through the list of slots. The following table describes the type and behavior of each iterator adaptor required. Note that this is only a conceptual model: the implementation collapses all these layers into a single iterator adaptor because several popular compilers failed to compile the implementation of the conceptual model.
Purpose | |
---|---|
An iterator through the list of slots connected to a signal. The
value_type of this iterator will be
std::pair<any, connection> , where the
any contains an instance of the slot function
type. |
|
This filtering iterator adaptor filters out slots that have been disconnected, so we never see a disconnected slot in later stages. | |
The projection iterator adaptor returns a reference to the first
member of the pair that constitutes a connected slot
(e.g., just the boost::any object that holds
the slot function). |
|
This transform iterator adaptor performs an
any_cast to extract a reference to the slot
function with the appropriate slot function type. |
|
This transform iterator adaptor calls the function object returned by dereferencing the underlying iterator with the set of arguments given to the signal itself, and returns the result of that slot call. | |
This iterator adaptor caches the result of dereferencing the underlying iterator. Therefore, dereferencing this iterator multiple times will only result in the underlying iterator being dereferenced once; thus, a slot can only be called once but its result can be used multiple times. | |
visit_each
function template The visit_each
function template is a mechanism for discovering objects that
are stored within another object. Function template
visit_each
takes three arguments: an object to
explore, a visitor function object that is invoked with each
subobject, and the int
0.
The third parameter is merely a temporary solution to the
widespread lack of proper function template partial
ordering. The primary visit_each
function template
specifies this third parameter type to be long
,
whereas any user specializations must specify their third
parameter to be of type int
. Thus, even though a
broken compiler cannot tell the ordering between, e.g., a
match against a parameter T
and a parameter
A<T>
, it can determine that the conversion
from the integer 0 to int
is better than the
conversion to long
. The ordering determined by this
conversion thus achieves partial ordering of the function
templates in a limited, but successful, way. The following
example illustrates the use of this technique:
template<typename> class A {}; template<typename T> void foo(T, long); template<typename T> void foo(A<T>, int); A<T> at; foo(at, 0);
In this example, we assume that our compiler can not tell that
A<T>
is a better match than T
,
and therefore assume that the function templates cannot be
ordered based on that parameter. Then the conversion from 0 to
int
is better than the conversion from 0 to
long
, and the second function template is chosen.