]>
Commit | Line | Data |
---|---|---|
7c673cae FG |
1 | <?xml version="1.0" encoding="utf-8"?> |
2 | <!DOCTYPE section PUBLIC "-//Boost//DTD BoostBook XML V1.0//EN" | |
3 | "http://www.boost.org/tools/boostbook/dtd/boostbook.dtd"> | |
4 | <section last-revision="$Date$"> | |
5 | <title>Design Rationale</title> | |
6 | ||
7 | <using-namespace name="boost"/> | |
8 | <using-namespace name="boost::signals"/> | |
9 | <using-class name="boost::signalN"/> | |
10 | ||
11 | <section> | |
12 | <title>Choice of Slot Definitions</title> | |
13 | ||
14 | <para> The definition of a slot differs amongst signals and slots | |
15 | libraries. Within Boost.Signals, a slot is defined in a very loose | |
16 | manner: it can be any function object that is callable given | |
17 | parameters of the types specified by the signal, and whose return | |
18 | value is convertible to the result type expected by the | |
19 | signal. However, alternative definitions have associated pros and | |
20 | cons that were considered prior to the construction of | |
21 | Boost.Signals.</para> | |
22 | ||
23 | <itemizedlist> | |
24 | <listitem> | |
25 | <para><emphasis role="bold">Slots derive from a specific base | |
26 | class</emphasis>: generally a scheme such as this will require | |
27 | all user-defined slots to derive from some library-specified | |
28 | <code>Slot</code> abstract class that defines a virtual | |
29 | function calling the slot. Adaptors can be used to convert a | |
30 | definition such as this to a definition similar to that used | |
31 | by Boost.Signals, but the use of a large number of small | |
32 | adaptor classes containing virtual functions has been found to | |
33 | cause an unacceptable increase in the size of executables | |
34 | (polymorphic class types require more code than | |
35 | non-polymorphic types).</para> | |
36 | ||
37 | <para> This approach does have the benefit of simplicity of | |
38 | implementation and user interface, from an object-oriented | |
39 | perspective.</para> | |
40 | </listitem> | |
41 | ||
42 | <listitem> | |
43 | <para><emphasis role="bold">Slots constructed from a set of | |
44 | primitives</emphasis>: in this scheme the slot can have a | |
45 | limited set of types (often derived from a common abstract | |
46 | base class) that are constructed from some library-defined set | |
47 | of primitives that often include conversions from free | |
48 | function pointers and member function pointers, and a limited | |
49 | set of binding capabilities. Such an approach is reasonably | |
50 | simple and cover most common cases, but it does not allow a | |
51 | large degree of flexibility in slot construction. Libraries | |
52 | for function object composition have become quite advanced and | |
53 | it is out of the scope of a signals and slots library to | |
54 | encorporate such enhancements. Thus Boost.Signals does not | |
55 | include argument binding or function object composition | |
56 | primitives, but instead provides a hook (via the | |
57 | <code><functionname>visit_each</functionname></code> | |
58 | mechanism) that allows existing binder/composition libraries | |
59 | to provide the necessary information to Signals.</para> | |
60 | </listitem> | |
61 | </itemizedlist> | |
62 | ||
63 | <para> Users not satisfied with the slot definition choice may opt | |
64 | to replace the default slot function type with an alternative that | |
65 | meets their specific needs.</para> | |
66 | </section> | |
67 | ||
68 | <section> | |
69 | <title>User-level Connection Management</title> | |
70 | ||
71 | <para> Users need to have fine control over the connection of | |
72 | signals to slots and their eventual disconnection. The approach | |
73 | taken by Boost.Signals is to return a | |
74 | <code><classname>connection</classname></code> object that enables | |
75 | connected/disconnected query, manual disconnection, and an | |
76 | automatic disconnection on destruction mode. Some other possible | |
77 | interfaces include:</para> | |
78 | ||
79 | <itemizedlist> | |
80 | <listitem> | |
81 | <para><emphasis role="bold">Pass slot to | |
82 | disconnect</emphasis>: in this interface model, the | |
83 | disconnection of a slot connected with | |
84 | <code>sig.<methodname>connect</methodname>(slot)</code> is | |
85 | performed via | |
86 | <code>sig.<methodname>disconnect</methodname>(slot)</code>. Internally, | |
87 | a linear search using slot comparison is performed and the | |
88 | slot, if found, is removed from the list. Unfortunately, | |
89 | querying connectedness will generally also end up as | |
90 | linear-time operations. This model also fails for | |
91 | implementation reasons when slots become more complex than | |
92 | simple function pointers, member function pointers and a | |
93 | limited set of compositions and argument binders: to match the | |
94 | slot given in the call to | |
95 | <code><methodname>disconnect</methodname></code> with an | |
96 | existing slot we would need to be able to compare arbitrary | |
97 | function objects, which is not feasible.</para> | |
98 | </listitem> | |
99 | ||
100 | <listitem> | |
101 | <para><emphasis role="bold">Pass a token to | |
102 | disconnect</emphasis>: this approach identifies slots with a | |
103 | token that is easily comparable (e.g., a string), enabling | |
104 | slots to be arbitrary function objects. While this approach is | |
105 | essentially equivalent to the approach taken by Boost.Signals, | |
106 | it is possibly more error-prone for several reasons:</para> | |
107 | ||
108 | <itemizedlist> | |
109 | <listitem> | |
110 | <para>Connections and disconnections must be paired, so | |
111 | the problem becomes similar to the problems incurred when | |
112 | pairing <code>new</code> and <code>delete</code> for | |
113 | dynamic memory allocation. While errors of this sort would | |
114 | not be catastrophic for a signals and slots | |
115 | implementation, their detection is generally | |
116 | nontrivial.</para> | |
117 | </listitem> | |
118 | ||
119 | <listitem> | |
120 | <para>Tokens must be unique, otherwise two slots will have | |
121 | the same name and will be indistinguishable. In | |
122 | environments where many connections will be made | |
123 | dynamically, name generation becomes an additional task | |
124 | for the user. Uniqueness of tokens also results in an | |
125 | additional failure mode when attempting to connect a slot | |
126 | using a token that has already been used.</para> | |
127 | </listitem> | |
128 | ||
129 | <listitem> | |
130 | <para>More parameterization would be required, because the | |
131 | token type must be user-defined. Additional | |
132 | parameterization steepens the learning curver and | |
133 | overcomplicates a simple interface.</para> | |
134 | </listitem> | |
135 | </itemizedlist> | |
136 | ||
137 | <para> This type of interface is supported in Boost.Signals | |
138 | via the slot grouping mechanism. It augments the | |
139 | <code><classname>connection</classname></code> object-based | |
140 | connection management scheme.</para> | |
141 | </listitem> | |
142 | </itemizedlist> | |
143 | </section> | |
144 | ||
145 | <section> | |
146 | <title>Combiner Interface</title> | |
147 | ||
148 | <para> The Combiner interface was chosen to mimic a call to an | |
149 | algorithm in the C++ standard library. It is felt that by viewing | |
150 | slot call results as merely a sequence of values accessed by input | |
151 | iterators, the combiner interface would be most natural to a | |
152 | proficient C++ programmer. Competing interface design generally | |
153 | required the combiners to be constructed to conform to an | |
154 | interface that would be customized for (and limited to) the | |
155 | Signals library. While these interfaces are generally enable more | |
156 | straighforward implementation of the signals & slots | |
157 | libraries, the combiners are unfortunately not reusable (either in | |
158 | other signals & slots libraries or within other generic | |
159 | algorithms), and the learning curve is steepened slightly to learn | |
160 | the specific combiner interface.</para> | |
161 | ||
162 | <para> The Signals formulation of combiners is based on the | |
163 | combiner using the "pull" mode of communication, instead of the | |
164 | more complex "push" mechanism. With a "pull" mechanism, the | |
165 | combiner's state can be kept on the stack and in the program | |
166 | counter, because whenever new data is required (i.e., calling the | |
167 | next slot to retrieve its return value), there is a simple | |
168 | interface to retrieve that data immediately and without returning | |
169 | from the combiner's code. Contrast this with the "push" mechanism, | |
170 | where the combiner must keep all state in class members because | |
171 | the combiner's routines will be invoked for each signal | |
172 | called. Compare, for example, a combiner that returns the maximum | |
173 | element from calling the slots. If the maximum element ever | |
174 | exceeds 100, no more slots are to be called.</para> | |
175 | ||
176 | <informaltable> | |
177 | <tgroup cols="2" align="left"> | |
178 | <thead> | |
179 | <row> | |
180 | <entry><para>Pull</para></entry> | |
181 | <entry><para>Push</para></entry> | |
182 | </row> | |
183 | </thead> | |
184 | <tbody> | |
185 | <row> | |
186 | <entry> | |
187 | <programlisting> | |
188 | struct pull_max { | |
189 | typedef int result_type; | |
190 | ||
191 | template<typename InputIterator> | |
192 | result_type operator()(InputIterator first, | |
193 | InputIterator last) | |
194 | { | |
195 | if (first == last) | |
196 | throw std::runtime_error("Empty!"); | |
197 | ||
198 | int max_value = *first++; | |
199 | while(first != last && *first <= 100) { | |
200 | if (*first > max_value) | |
201 | max_value = *first; | |
202 | ++first; | |
203 | } | |
204 | ||
205 | return max_value; | |
206 | } | |
207 | }; | |
208 | </programlisting> | |
209 | </entry> | |
210 | <entry> | |
211 | <programlisting> | |
212 | struct push_max { | |
213 | typedef int result_type; | |
214 | ||
215 | push_max() : max_value(), got_first(false) {} | |
216 | ||
217 | // returns false when we want to stop | |
218 | bool operator()(int result) { | |
219 | if (result > 100) | |
220 | return false; | |
221 | ||
222 | if (!got_first) { | |
223 | got_first = true; | |
224 | max_value = result; | |
225 | return true; | |
226 | } | |
227 | ||
228 | if (result > max_value) | |
229 | max_value = result; | |
230 | ||
231 | return true; | |
232 | } | |
233 | ||
234 | int get_value() const | |
235 | { | |
236 | if (!got_first) | |
237 | throw std::runtime_error("Empty!"); | |
238 | return max_value; | |
239 | } | |
240 | ||
241 | private: | |
242 | int max_value; | |
243 | bool got_first; | |
244 | }; | |
245 | </programlisting> | |
246 | </entry> | |
247 | </row> | |
248 | </tbody> | |
249 | </tgroup> | |
250 | </informaltable> | |
251 | ||
252 | <para>There are several points to note in these examples. The | |
253 | "pull" version is a reusable function object that is based on an | |
254 | input iterator sequence with an integer <code>value_type</code>, | |
255 | and is very straightforward in design. The "push" model, on the | |
256 | other hand, relies on an interface specific to the caller and is | |
257 | not generally reusable. It also requires extra state values to | |
258 | determine, for instance, if any elements have been | |
259 | received. Though code quality and ease-of-use is generally | |
260 | subjective, the "pull" model is clearly shorter and more reusable | |
261 | and will often be construed as easier to write and understand, | |
262 | even outside the context of a signals & slots library.</para> | |
263 | ||
264 | <para> The cost of the "pull" combiner interface is paid in the | |
265 | implementation of the Signals library itself. To correctly handle | |
266 | slot disconnections during calls (e.g., when the dereference | |
267 | operator is invoked), one must construct the iterator to skip over | |
268 | disconnected slots. Additionally, the iterator must carry with it | |
269 | the set of arguments to pass to each slot (although a reference to | |
270 | a structure containing those arguments suffices), and must cache | |
271 | the result of calling the slot so that multiple dereferences don't | |
272 | result in multiple calls. This apparently requires a large degree | |
273 | of overhead, though if one considers the entire process of | |
274 | invoking slots one sees that the overhead is nearly equivalent to | |
275 | that in the "push" model, but we have inverted the control | |
276 | structures to make iteration and dereference complex (instead of | |
277 | making combiner state-finding complex).</para> | |
278 | </section> | |
279 | ||
280 | <section> | |
281 | <title>Connection Interfaces: += operator</title> | |
282 | ||
283 | <para> Boost.Signals supports a connection syntax with the form | |
284 | <code>sig.<methodname>connect</methodname>(slot)</code>, but a | |
285 | more terse syntax <code>sig += slot</code> has been suggested (and | |
286 | has been used by other signals & slots implementations). There | |
287 | are several reasons as to why this syntax has been | |
288 | rejected:</para> | |
289 | ||
290 | <itemizedlist> | |
291 | <listitem> | |
292 | <para><emphasis role="bold">It's unnecessary</emphasis>: the | |
293 | connection syntax supplied by Boost.Signals is no less | |
294 | powerful that that supplied by the <code>+=</code> | |
295 | operator. The savings in typing (<code>connect()</code> | |
296 | vs. <code>+=</code>) is essentially negligible. Furthermore, | |
297 | one could argue that calling <code>connect()</code> is more | |
298 | readable than an overload of <code>+=</code>.</para> | |
299 | </listitem> | |
300 | <listitem> | |
301 | <para><emphasis role="bold">Ambiguous return type</emphasis>: | |
302 | there is an ambiguity concerning the return value of the | |
303 | <code>+=</code> operation: should it be a reference to the | |
304 | signal itself, to enable <code>sig += slot1 += slot2</code>, | |
305 | or should it return a | |
306 | <code><classname>connection</classname></code> for the | |
307 | newly-created signal/slot connection?</para> | |
308 | </listitem> | |
309 | ||
310 | <listitem> | |
311 | <para><emphasis role="bold">Gateway to operators -=, | |
312 | +</emphasis>: when one has added a connection operator | |
313 | <code>+=</code>, it seems natural to have a disconnection | |
314 | operator <code>-=</code>. However, this presents problems when | |
315 | the library allows arbitrary function objects to implicitly | |
316 | become slots, because slots are no longer comparable. <!-- | |
317 | (see the discussion on this topic in User-level Connection | |
318 | Management). --></para> | |
319 | ||
320 | <para> The second obvious addition when one has | |
321 | <code>operator+=</code> would be to add a <code>+</code> | |
322 | operator that supports addition of multiple slots, followed by | |
323 | assignment to a signal. However, this would require | |
324 | implementing <code>+</code> such that it can accept any two | |
325 | function objects, which is technically infeasible.</para> | |
326 | </listitem> | |
327 | </itemizedlist> | |
328 | </section> | |
329 | ||
330 | <section> | |
331 | <title><code>trackable</code> rationale</title> | |
332 | ||
333 | <para> The <code><classname>trackable</classname></code> | |
334 | class is the primary user interface to automatic connection | |
335 | lifetime management, and its design affects users directly. Two | |
336 | issues stick out most: the odd copying behavior of | |
337 | <code>trackable</code>, and the limitation requiring users to | |
338 | derive from <code>trackable</code> to create types that can | |
339 | participate in automatic connection management.</para> | |
340 | ||
341 | <section> | |
342 | <title><code>trackable</code> copying behavior</title> | |
343 | ||
344 | <para> The copying behavior of | |
345 | <code><classname>trackable</classname></code> is essentially | |
346 | that <code><classname>trackable</classname></code> subobjects | |
347 | are never copied; instead, the copy operation is merely a | |
348 | no-op. To understand this, we look at the nature of a | |
349 | signal-slot connection and note that the connection is based on | |
350 | the entities that are being connected; when one of the entities | |
351 | is destroyed, the connection is destroyed. Therefore, when a | |
352 | <code><classname>trackable</classname></code> subobject is | |
353 | copied, we cannot copy the connections because the connections | |
354 | don't refer to the target entity - they refer to the source | |
355 | entity. This reason is dual to the reason signals are | |
356 | noncopyable: the slots connected to them are connected to that | |
357 | particular signal, not the data contained in the signal.</para> | |
358 | </section> | |
359 | ||
360 | <section> | |
361 | <title>Why derivation from <code>trackable</code>?</title> | |
362 | ||
363 | <para> For <code><classname>trackable</classname></code> to work | |
364 | properly, there are two constraints:</para> | |
365 | ||
366 | <itemizedlist> | |
367 | <listitem> | |
368 | <para><code><classname>trackable</classname></code> must | |
369 | have storage space to keep track of all connections made to | |
370 | this object.</para> | |
371 | </listitem> | |
372 | ||
373 | <listitem> | |
374 | <para><code><classname>trackable</classname></code> must be | |
375 | notified when the object is being destructed so that it can | |
376 | disconnect its connections.</para> | |
377 | </listitem> | |
378 | </itemizedlist> | |
379 | ||
380 | <para>Clearly, deriving from | |
381 | <code><classname>trackable</classname></code> meets these two | |
382 | guidelines. We have not yet found a superior solution.</para> | |
383 | </section> | |
384 | </section> | |
385 | ||
386 | <section> | |
387 | <title>Comparison with other Signal/Slot implementations</title> | |
388 | ||
389 | <section> | |
390 | <title>libsigc++</title> | |
391 | ||
392 | <para> <ulink | |
393 | url="http://libsigc.sourceforge.net">libsigc++</ulink> is a C++ | |
394 | signals & slots library that originally started as part of | |
395 | an initiative to wrap the C interfaces to <ulink | |
396 | url="http://www.gtk.org">GTK</ulink> libraries in C++, and has | |
397 | grown to be a separate library maintained by Karl Nelson. There | |
398 | are many similarities between libsigc++ and Boost.Signals, and | |
399 | indeed Boost.Signals was strongly influenced by Karl Nelson and | |
400 | libsigc++. A cursory inspection of each library will find a | |
401 | similar syntax for the construction of signals and in the use of | |
402 | connections and automatic connection lifetime management. There | |
403 | are some major differences in design that separate these | |
404 | libraries:</para> | |
405 | ||
406 | <itemizedlist> | |
407 | <listitem> | |
408 | <para><emphasis role="bold">Slot definitions</emphasis>: | |
409 | slots in libsigc++ are created using a set of primitives | |
410 | defined by the library. These primitives allow binding of | |
411 | objects (as part of the library), explicit adaptation from | |
412 | the argument and return types of the signal to the argument | |
413 | and return types of the slot (libsigc++ is, by default, more | |
414 | strict about types than Boost.Signals). A discussion of this | |
415 | approach with a comparison against the approach taken by | |
416 | Boost.Signals is given in Choice of Slot Definitions.</para> | |
417 | </listitem> | |
418 | ||
419 | <listitem> | |
420 | <para><emphasis role="bold">Combiner/Marshaller | |
421 | interface</emphasis>: the equivalent to Boost.Signals | |
422 | combiners in libsigc++ are the marshallers. Marshallers are | |
423 | similar to the "push" interface described in Combiner | |
424 | Interface, and a proper treatment of the topic is given | |
425 | there.</para> | |
426 | </listitem> | |
427 | </itemizedlist> | |
428 | </section> | |
429 | ||
430 | <section> | |
431 | <title>.NET delegates</title> | |
432 | ||
433 | <para> <ulink url="http://www.microsoft.com">Microsoft</ulink> | |
434 | has introduced the .NET Framework and an associated set of | |
435 | languages and language extensions, one of which is the | |
436 | delegate. Delegates are similar to signals and slots, but they | |
437 | are more limited than most C++ signals and slots implementations | |
438 | in that they:</para> | |
439 | ||
440 | <itemizedlist> | |
441 | <listitem> | |
442 | <para>Require exact type matches between a delegate and what | |
443 | it is calling.</para> | |
444 | </listitem> | |
445 | ||
446 | <listitem><para>Only return the result of the last target called, with no option for customization.</para></listitem> | |
447 | <listitem> | |
448 | <para>Must call a method with <code>this</code> already | |
449 | bound.</para> | |
450 | </listitem> | |
451 | </itemizedlist> | |
452 | </section> | |
453 | </section> | |
454 | </section> |