1 // Written in the D programming language.
2 
3 /**
4  * Signals and Slots are an implementation of the Observer Pattern.
5  * Essentially, when a Signal is emitted, a list of connected Observers
6  * (called slots) are called.
7  *
8  * There have been several D implementations of Signals and Slots.
9  * This version makes use of several new features in D, which make
10  * using it simpler and less error prone. In particular, it is no
11  * longer necessary to instrument the slots.
12  *
13  * References:
14  *      $(LUCKY A Deeper Look at Signals and Slots)$(BR)
15  *      $(LINK2 http://en.wikipedia.org/wiki/Observer_pattern, Observer pattern)$(BR)
16  *      $(LINK2 http://en.wikipedia.org/wiki/Signals_and_slots, Wikipedia)$(BR)
17  *      $(LINK2 http://boost.org/doc/html/$(SIGNALS).html, Boost Signals)$(BR)
18  *      $(LINK2 http://qt-project.org/doc/qt-5/signalsandslots.html, Qt)$(BR)
19  *
20  *      There has been a great deal of discussion in the D newsgroups
21  *      over this, and several implementations:
22  *
23  *      $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/announce/signal_slots_library_4825.html, signal slots library)$(BR)
24  *      $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/Signals_and_Slots_in_D_42387.html, Signals and Slots in D)$(BR)
25  *      $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/Dynamic_binding_--_Qt_s_Signals_and_Slots_vs_Objective-C_42260.html, Dynamic binding -- Qt's Signals and Slots vs Objective-C)$(BR)
26  *      $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/Dissecting_the_SS_42377.html, Dissecting the SS)$(BR)
27  *      $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/dwt/about_harmonia_454.html, about harmonia)$(BR)
28  *      $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/announce/1502.html, Another event handling module)$(BR)
29  *      $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/41825.html, Suggestion: signal/slot mechanism)$(BR)
30  *      $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/13251.html, Signals and slots?)$(BR)
31  *      $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/10714.html, Signals and slots ready for evaluation)$(BR)
32  *      $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/1393.html, Signals & Slots for Walter)$(BR)
33  *      $(LINK2 http://www.digitalmars.com/d/archives/28456.html, Signal/Slot mechanism?)$(BR)
34  *      $(LINK2 http://www.digitalmars.com/d/archives/19470.html, Modern Features?)$(BR)
35  *      $(LINK2 http://www.digitalmars.com/d/archives/16592.html, Delegates vs interfaces)$(BR)
36  *      $(LINK2 http://www.digitalmars.com/d/archives/16583.html, The importance of component programming (properties$(COMMA) signals and slots$(COMMA) etc))$(BR)
37  *      $(LINK2 http://www.digitalmars.com/d/archives/16368.html, signals and slots)$(BR)
38  *
39  * Bugs:
40  *      $(RED Slots can only be delegates referring directly to
41  *      class or interface member functions. If a delegate to something else
42  *      is passed to connect(), such as a struct member function,
43  *      a nested function, a COM interface, a closure, undefined behavior
44  *      will result.)
45  *
46  *      Not safe for multiple threads operating on the same signals
47  *      or slots.
48  * Macros:
49  *      SIGNALS=signals
50  *
51  * Copyright: Copyright The D Language Foundation 2000 - 2009.
52  * License:   $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
53  * Authors:   $(HTTP digitalmars.com, Walter Bright)
54  * Source:    $(PHOBOSSRC std/signals.d)
55  *
56  * $(SCRIPT inhibitQuickIndex = 1;)
57  */
58 /*          Copyright The D Language Foundation 2000 - 2009.
59  * Distributed under the Boost Software License, Version 1.0.
60  *    (See accompanying file LICENSE_1_0.txt or copy at
61  *          http://www.boost.org/LICENSE_1_0.txt)
62  */
63 module std.signals;
64 
65 import core.exception : onOutOfMemoryError;
66 import core.stdc.stdlib : calloc, realloc, free;
67 import std.stdio;
68 
69 // Special function for internal use only.
70 // Use of this is where the slot had better be a delegate
71 // to an object or an interface that is part of an object.
72 extern (C) Object _d_toObject(void* p);
73 
74 // Used in place of Object.notifyRegister and Object.notifyUnRegister.
75 alias DisposeEvt = void delegate(Object);
76 extern (C) void  rt_attachDisposeEvent( Object obj, DisposeEvt evt );
77 extern (C) void  rt_detachDisposeEvent( Object obj, DisposeEvt evt );
78 //debug=signal;
79 
80 /************************
81  * Mixin to create a signal within a class object.
82  *
83  * Different signals can be added to a class by naming the mixins.
84  */
85 
86 mixin template Signal(T1...)
87 {
88     static import core.exception;
89     static import core.stdc.stdlib;
90     /***
91      * A slot is implemented as a delegate.
92      * The slot_t is the type of the delegate.
93      * The delegate must be to an instance of a class or an interface
94      * to a class instance.
95      * Delegates to struct instances or nested functions must not be
96      * used as slots. This applies even if the nested function does not access
97      * it's parent function variables.
98      */
99     alias slot_t = void delegate(T1);
100 
101     /***
102      * Call each of the connected slots, passing the argument(s) i to them.
103      * Nested call will be ignored.
104      */
105     final void emit( T1 i )
106     {
107         if (status >= ST.inemitting || !slots.length)
108             return; // should not nest
109 
110         status = ST.inemitting;
111         scope (exit)
112             status = ST.idle;
113 
114         foreach (slot; slots[0 .. slots_idx])
115         {   if (slot)
116                 slot(i);
117         }
118 
119         assert(status >= ST.inemitting);
120         if (status == ST.inemitting_disconnected)
121         {
122             for (size_t j = 0; j < slots_idx;)
123             {
124                 if (slots[j] is null)
125                 {
126                     slots_idx--;
127                     slots[j] = slots[slots_idx];
128                 }
129                 else
130                     j++;
131             }
132         }
133     }
134 
135     /***
136      * Add a slot to the list of slots to be called when emit() is called.
137      */
138     final void connect(slot_t slot)
139     {
140         /* Do this:
141          *    slots ~= slot;
142          * but use malloc() and friends instead
143          */
144         auto len = slots.length;
145         if (slots_idx == len)
146         {
147             if (slots.length == 0)
148             {
149                 len = 4;
150                 auto p = core.stdc.stdlib.calloc(slot_t.sizeof, len);
151                 if (!p)
152                     core.exception.onOutOfMemoryError();
153                 slots = (cast(slot_t*) p)[0 .. len];
154             }
155             else
156             {
157                 import core.checkedint : addu, mulu;
158                 bool overflow;
159                 len = addu(mulu(len, 2, overflow), 4, overflow); // len = len * 2 + 4
160                 const nbytes = mulu(len, slot_t.sizeof, overflow);
161                 if (overflow) assert(0);
162 
163                 auto p = core.stdc.stdlib.realloc(slots.ptr, nbytes);
164                 if (!p)
165                     core.exception.onOutOfMemoryError();
166                 slots = (cast(slot_t*) p)[0 .. len];
167                 slots[slots_idx + 1 .. $] = null;
168             }
169         }
170         slots[slots_idx++] = slot;
171 
172      L1:
173         Object o = _d_toObject(slot.ptr);
174         rt_attachDisposeEvent(o, &unhook);
175     }
176 
177     /***
178      * Remove a slot from the list of slots to be called when emit() is called.
179      */
180     final void disconnect(slot_t slot)
181     {
182         debug (signal) writefln("Signal.disconnect(slot)");
183         size_t disconnectedSlots = 0;
184         size_t instancePreviousSlots = 0;
185         if (status >= ST.inemitting)
186         {
187             foreach (i, sloti; slots[0 .. slots_idx])
188             {
189                 if (sloti.ptr == slot.ptr &&
190                     ++instancePreviousSlots &&
191                     sloti == slot)
192                 {
193                     disconnectedSlots++;
194                     slots[i] = null;
195                     status = ST.inemitting_disconnected;
196                 }
197             }
198         }
199         else
200         {
201             for (size_t i = 0; i < slots_idx; )
202             {
203                 if (slots[i].ptr == slot.ptr &&
204                     ++instancePreviousSlots &&
205                     slots[i] == slot)
206                 {
207                     slots_idx--;
208                     disconnectedSlots++;
209                     slots[i] = slots[slots_idx];
210                     slots[slots_idx] = null;        // not strictly necessary
211                 }
212                 else
213                     i++;
214             }
215         }
216 
217          // detach object from dispose event if all its slots have been removed
218         if (instancePreviousSlots == disconnectedSlots)
219         {
220             Object o = _d_toObject(slot.ptr);
221             rt_detachDisposeEvent(o, &unhook);
222         }
223      }
224 
225     /***
226      * Disconnect all the slots.
227      */
228     final void disconnectAll()
229     {
230         debug (signal) writefln("Signal.disconnectAll");
231         __dtor();
232         slots_idx = 0;
233         status = ST.idle;
234     }
235 
236     /* **
237      * Special function called when o is destroyed.
238      * It causes any slots dependent on o to be removed from the list
239      * of slots to be called by emit().
240      */
241     final void unhook(Object o)
242     in { assert( status == ST.idle ); }
243     do
244     {
245         debug (signal) writefln("Signal.unhook(o = %s)", cast(void*) o);
246         for (size_t i = 0; i < slots_idx; )
247         {
248             if (_d_toObject(slots[i].ptr) is o)
249             {   slots_idx--;
250                 slots[i] = slots[slots_idx];
251                 slots[slots_idx] = null;        // not strictly necessary
252             }
253             else
254                 i++;
255         }
256     }
257 
258     /* **
259      * There can be multiple destructors inserted by mixins.
260      */
261     ~this()
262     {
263         /* **
264          * When this object is destroyed, need to let every slot
265          * know that this object is destroyed so they are not left
266          * with dangling references to it.
267          */
268         if (slots.length)
269         {
270             foreach (slot; slots[0 .. slots_idx])
271             {
272                 if (slot)
273                 {   Object o = _d_toObject(slot.ptr);
274                     rt_detachDisposeEvent(o, &unhook);
275                 }
276             }
277             core.stdc.stdlib.free(slots.ptr);
278             slots = null;
279         }
280     }
281 
282   private:
283     slot_t[] slots;             // the slots to call from emit()
284     size_t slots_idx;           // used length of slots[]
285 
286     enum ST { idle, inemitting, inemitting_disconnected }
287     ST status;
288 }
289 
290 ///
291 @system unittest
292 {
293     import std.signals;
294 
295     int observedMessageCounter = 0;
296 
297     class Observer
298     {   // our slot
299         void watch(string msg, int value)
300         {
301             switch (observedMessageCounter++)
302             {
303                 case 0:
304                     assert(msg == "setting new value");
305                     assert(value == 4);
306                     break;
307                 case 1:
308                     assert(msg == "setting new value");
309                     assert(value == 6);
310                     break;
311                 default:
312                     assert(0, "Unknown observation");
313             }
314         }
315     }
316 
317     class Observer2
318     {   // our slot
319         void watch(string msg, int value)
320         {
321         }
322     }
323 
324     class Foo
325     {
326         int value() { return _value; }
327 
328         int value(int v)
329         {
330             if (v != _value)
331             {   _value = v;
332                 // call all the connected slots with the two parameters
333                 emit("setting new value", v);
334             }
335             return v;
336         }
337 
338         // Mix in all the code we need to make Foo into a signal
339         mixin Signal!(string, int);
340 
341       private :
342         int _value;
343     }
344 
345     Foo a = new Foo;
346     Observer o = new Observer;
347     auto o2 = new Observer2;
348     auto o3 = new Observer2;
349     auto o4 = new Observer2;
350     auto o5 = new Observer2;
351 
352     a.value = 3;                // should not call o.watch()
353     a.connect(&o.watch);        // o.watch is the slot
354     a.connect(&o2.watch);
355     a.connect(&o3.watch);
356     a.connect(&o4.watch);
357     a.connect(&o5.watch);
358     a.value = 4;                // should call o.watch()
359     a.disconnect(&o.watch);     // o.watch is no longer a slot
360     a.disconnect(&o3.watch);
361     a.disconnect(&o5.watch);
362     a.disconnect(&o4.watch);
363     a.disconnect(&o2.watch);
364     a.value = 5;                // so should not call o.watch()
365     a.connect(&o2.watch);
366     a.connect(&o.watch);        // connect again
367     a.value = 6;                // should call o.watch()
368     destroy(o);                 // destroying o should automatically disconnect it
369     a.value = 7;                // should not call o.watch()
370 
371     assert(observedMessageCounter == 2);
372 }
373 
374 // A function whose sole purpose is to get this module linked in
375 // so the unittest will run.
376 void linkin() { }
377 
378 @system unittest
379 {
380     class Observer
381     {
382         void watch(string msg, int i)
383         {
384             //writefln("Observed msg '%s' and value %s", msg, i);
385             captured_value = i;
386             captured_msg   = msg;
387         }
388 
389         int    captured_value;
390         string captured_msg;
391     }
392 
393     class Foo
394     {
395         @property int value() { return _value; }
396 
397         @property int value(int v)
398         {
399             if (v != _value)
400             {   _value = v;
401                 emit("setting new value", v);
402             }
403             return v;
404         }
405 
406         mixin Signal!(string, int);
407 
408       private:
409         int _value;
410     }
411 
412     Foo a = new Foo;
413     Observer o = new Observer;
414 
415     // check initial condition
416     assert(o.captured_value == 0);
417     assert(o.captured_msg == "");
418 
419     // set a value while no observation is in place
420     a.value = 3;
421     assert(o.captured_value == 0);
422     assert(o.captured_msg == "");
423 
424     // connect the watcher and trigger it
425     a.connect(&o.watch);
426     a.value = 4;
427     assert(o.captured_value == 4);
428     assert(o.captured_msg == "setting new value");
429 
430     // disconnect the watcher and make sure it doesn't trigger
431     a.disconnect(&o.watch);
432     a.value = 5;
433     assert(o.captured_value == 4);
434     assert(o.captured_msg == "setting new value");
435 
436     // reconnect the watcher and make sure it triggers
437     a.connect(&o.watch);
438     a.value = 6;
439     assert(o.captured_value == 6);
440     assert(o.captured_msg == "setting new value");
441 
442     // destroy the underlying object and make sure it doesn't cause
443     // a crash or other problems
444     destroy(o);
445     a.value = 7;
446 }
447 
448 @system unittest
449 {
450     class Observer
451     {
452         int    i;
453         long   l;
454         string str;
455 
456         void watchInt(string str, int i)
457         {
458             this.str = str;
459             this.i = i;
460         }
461 
462         void watchLong(string str, long l)
463         {
464             this.str = str;
465             this.l = l;
466         }
467     }
468 
469     class Bar
470     {
471         @property void value1(int v)  { s1.emit("str1", v); }
472         @property void value2(int v)  { s2.emit("str2", v); }
473         @property void value3(long v) { s3.emit("str3", v); }
474 
475         mixin Signal!(string, int)  s1;
476         mixin Signal!(string, int)  s2;
477         mixin Signal!(string, long) s3;
478     }
479 
480     void test(T)(T a) {
481         auto o1 = new Observer;
482         auto o2 = new Observer;
483         auto o3 = new Observer;
484 
485         // connect the watcher and trigger it
486         a.s1.connect(&o1.watchInt);
487         a.s2.connect(&o2.watchInt);
488         a.s3.connect(&o3.watchLong);
489 
490         assert(!o1.i && !o1.l && o1.str == null);
491         assert(!o2.i && !o2.l && o2.str == null);
492         assert(!o3.i && !o3.l && o3.str == null);
493 
494         a.value1 = 11;
495         assert(o1.i == 11 && !o1.l && o1.str == "str1");
496         assert(!o2.i && !o2.l && o2.str == null);
497         assert(!o3.i && !o3.l && o3.str == null);
498         o1.i = -11; o1.str = "x1";
499 
500         a.value2 = 12;
501         assert(o1.i == -11 && !o1.l && o1.str == "x1");
502         assert(o2.i == 12 && !o2.l && o2.str == "str2");
503         assert(!o3.i && !o3.l && o3.str == null);
504         o2.i = -12; o2.str = "x2";
505 
506         a.value3 = 13;
507         assert(o1.i == -11 && !o1.l && o1.str == "x1");
508         assert(o2.i == -12 && !o1.l && o2.str == "x2");
509         assert(!o3.i && o3.l == 13 && o3.str == "str3");
510         o3.l = -13; o3.str = "x3";
511 
512         // disconnect the watchers and make sure it doesn't trigger
513         a.s1.disconnect(&o1.watchInt);
514         a.s2.disconnect(&o2.watchInt);
515         a.s3.disconnect(&o3.watchLong);
516 
517         a.value1 = 21;
518         a.value2 = 22;
519         a.value3 = 23;
520         assert(o1.i == -11 && !o1.l && o1.str == "x1");
521         assert(o2.i == -12 && !o1.l && o2.str == "x2");
522         assert(!o3.i && o3.l == -13 && o3.str == "x3");
523 
524         // reconnect the watcher and make sure it triggers
525         a.s1.connect(&o1.watchInt);
526         a.s2.connect(&o2.watchInt);
527         a.s3.connect(&o3.watchLong);
528 
529         a.value1 = 31;
530         a.value2 = 32;
531         a.value3 = 33;
532         assert(o1.i == 31 && !o1.l && o1.str == "str1");
533         assert(o2.i == 32 && !o1.l && o2.str == "str2");
534         assert(!o3.i && o3.l == 33 && o3.str == "str3");
535 
536         // destroy observers
537         destroy(o1);
538         destroy(o2);
539         destroy(o3);
540         a.value1 = 41;
541         a.value2 = 42;
542         a.value3 = 43;
543     }
544 
545     test(new Bar);
546 
547     class BarDerived: Bar
548     {
549         @property void value4(int v)  { s4.emit("str4", v); }
550         @property void value5(int v)  { s5.emit("str5", v); }
551         @property void value6(long v) { s6.emit("str6", v); }
552 
553         mixin Signal!(string, int)  s4;
554         mixin Signal!(string, int)  s5;
555         mixin Signal!(string, long) s6;
556     }
557 
558     auto a = new BarDerived;
559 
560     test!Bar(a);
561     test!BarDerived(a);
562 
563     auto o4 = new Observer;
564     auto o5 = new Observer;
565     auto o6 = new Observer;
566 
567     // connect the watcher and trigger it
568     a.s4.connect(&o4.watchInt);
569     a.s5.connect(&o5.watchInt);
570     a.s6.connect(&o6.watchLong);
571 
572     assert(!o4.i && !o4.l && o4.str == null);
573     assert(!o5.i && !o5.l && o5.str == null);
574     assert(!o6.i && !o6.l && o6.str == null);
575 
576     a.value4 = 44;
577     assert(o4.i == 44 && !o4.l && o4.str == "str4");
578     assert(!o5.i && !o5.l && o5.str == null);
579     assert(!o6.i && !o6.l && o6.str == null);
580     o4.i = -44; o4.str = "x4";
581 
582     a.value5 = 45;
583     assert(o4.i == -44 && !o4.l && o4.str == "x4");
584     assert(o5.i == 45 && !o5.l && o5.str == "str5");
585     assert(!o6.i && !o6.l && o6.str == null);
586     o5.i = -45; o5.str = "x5";
587 
588     a.value6 = 46;
589     assert(o4.i == -44 && !o4.l && o4.str == "x4");
590     assert(o5.i == -45 && !o4.l && o5.str == "x5");
591     assert(!o6.i && o6.l == 46 && o6.str == "str6");
592     o6.l = -46; o6.str = "x6";
593 
594     // disconnect the watchers and make sure it doesn't trigger
595     a.s4.disconnect(&o4.watchInt);
596     a.s5.disconnect(&o5.watchInt);
597     a.s6.disconnect(&o6.watchLong);
598 
599     a.value4 = 54;
600     a.value5 = 55;
601     a.value6 = 56;
602     assert(o4.i == -44 && !o4.l && o4.str == "x4");
603     assert(o5.i == -45 && !o4.l && o5.str == "x5");
604     assert(!o6.i && o6.l == -46 && o6.str == "x6");
605 
606     // reconnect the watcher and make sure it triggers
607     a.s4.connect(&o4.watchInt);
608     a.s5.connect(&o5.watchInt);
609     a.s6.connect(&o6.watchLong);
610 
611     a.value4 = 64;
612     a.value5 = 65;
613     a.value6 = 66;
614     assert(o4.i == 64 && !o4.l && o4.str == "str4");
615     assert(o5.i == 65 && !o4.l && o5.str == "str5");
616     assert(!o6.i && o6.l == 66 && o6.str == "str6");
617 
618     // destroy observers
619     destroy(o4);
620     destroy(o5);
621     destroy(o6);
622     a.value4 = 44;
623     a.value5 = 45;
624     a.value6 = 46;
625 }
626 
627 // Triggers bug from https://issues.dlang.org/show_bug.cgi?id=15341
628 @system unittest
629 {
630     class Observer
631     {
632        void watch() { }
633        void watch2() { }
634     }
635 
636     class Bar
637     {
638        mixin Signal!();
639     }
640 
641    auto a = new Bar;
642    auto o = new Observer;
643 
644    //Connect both observer methods for the same instance
645    a.connect(&o.watch);
646    a.connect(&o.watch2); // not connecting watch2() or disconnecting it manually fixes the issue
647 
648    //Disconnect a single method of the two
649    a.disconnect(&o.watch); // NOT disconnecting watch() fixes the issue
650 
651    destroy(o); // destroying o should automatically call unhook and disconnect the slot for watch2
652    a.emit(); // should not raise segfault since &o.watch2 is no longer connected
653 }
654 
655 version (none) // Disabled because of https://issues.dlang.org/show_bug.cgi?id=5028
656 @system unittest
657 {
658     class A
659     {
660         mixin Signal!(string, int) s1;
661     }
662 
663     class B : A
664     {
665         mixin Signal!(string, int) s2;
666     }
667 }
668 
669 // Triggers bug from https://issues.dlang.org/show_bug.cgi?id=16249
670 @system unittest
671 {
672     class myLINE
673     {
674         mixin Signal!( myLINE, int );
675 
676         void value( int v )
677         {
678             if ( v >= 0 ) emit( this, v );
679             else          emit( new myLINE, v );
680         }
681     }
682 
683     class Dot
684     {
685         int value;
686 
687         myLINE line_;
688         void line( myLINE line_x )
689         {
690             if ( line_ is line_x ) return;
691 
692             if ( line_ !is null )
693             {
694                 line_.disconnect( &watch );
695             }
696             line_ = line_x;
697             line_.connect( &watch );
698         }
699 
700         void watch( myLINE line_x, int value_x )
701         {
702             line = line_x;
703             value = value_x;
704         }
705     }
706 
707     auto dot1 = new Dot;
708     auto dot2 = new Dot;
709     auto line = new myLINE;
710     dot1.line = line;
711     dot2.line = line;
712 
713     line.value = 11;
714     assert( dot1.value == 11 );
715     assert( dot2.value == 11 );
716 
717     line.value = -22;
718     assert( dot1.value == -22 );
719     assert( dot2.value == -22 );
720 }
721 
722 @system unittest
723 {
724     import std.signals;
725 
726     class Observer
727     {   // our slot
728         void watch(string msg, int value)
729         {
730             if (value != 0)
731             {
732                 assert(msg == "setting new value");
733                 assert(value == 1);
734             }
735         }
736     }
737 
738     class Foo
739     {
740         int value() { return _value; }
741 
742         int value(int v)
743         {
744             if (v != _value)
745             {
746                 _value = v;
747                 // call all the connected slots with the parameters
748                 emit("setting new value", v);
749             }
750             return v;
751         }
752 
753         // Mix in all the code we need to make Foo into a signal
754         mixin Signal!(string, int);
755 
756       private :
757         int _value;
758     }
759 
760     Foo a = new Foo;
761     Observer o = new Observer;
762     auto o2 = new Observer;
763 
764     a.value = 3;                // should not call o.watch()
765     a.connect(&o.watch);        // o.watch is the slot
766     a.connect(&o2.watch);
767     a.value = 1;                // should call o.watch()
768     a.disconnectAll();
769     a.value = 5;                // so should not call o.watch()
770     a.connect(&o.watch);        // connect again
771     a.connect(&o2.watch);
772     a.value = 1;                // should call o.watch()
773     destroy(o);                 // destroying o should automatically disconnect it
774     destroy(o2);
775     a.value = 7;                // should not call o.watch()
776 }