The SlotSig library Tutorial

Content :

General

All the needed files are in the directory named slotsig, which contains the file <slotsig_base.h>, and many files <slotsig_nn.h>, where nn is a number between 0 and 16 (included). To use the library, you need to include one of those files. Choose the appropriate one according to the number of parameters you want to send through your signals.

Because the library uses the Standard Template Library, you may need to link against it. For example, using g++, and assuming the slotsig directory is in /path/to/, you compile and link a program using :

g++ -Wall -o program -I/path/to -lstdc++ program.cpp /path/to/slotsig_bases.cpp

Please note the last parameter : the file slotsig_bases.cpp must be compiled and linked with your project. For now SlotSig provides no facility to use it as an external library (it may change in the future though).

In the directory containing this file you're currently reading, you should find a Makefile. Just invoke

$ make

from this directory, and all the following example programs should be compiled, without any warning or error.

First step : connect a signal to a global function

Consider this small program :

01: #include <iostream>
02: #include <slotsig/slotsig_0.h>
03: void f()
04: { std::cout << "::f()\n" ; }
05: int main (int argc, char* argv[])
06: {
07:   SlotSig::Sig0<void> sig ;
08:   sig.connect(f) ;
09:   sig.run() ;
10:   return 0 ;
11: }

(code here)

When run, this program will print the single line :

::f()

Let's explain. First we create a signal, called sig (ligne 7), whose type is Sig0<void> (in the namespace SlotSig). The 0 means, this signal will handle slots which do not receive any parameters. The void is the return type of these slots. So, to this signal we can connect functions that do not take any parameters and return void (try to change the void line 3 to something else, the program won't compile).

Lines 3-4, we have a global function which returns void and doesn't take any parameters : so it can be connected to our signal, thus becoming a slot. This connection is done line 8, using a method in the Sig0<> class named connect(). Simply give the pointer to the function you want to connect. To give it a try, define more such functions and connect them using the same syntax.

When the signal is emitted, using the run() method in Sig0<>, all the connected functions will be called. Here the function simply prints a small message. One very important thing to note :

the functions are called in arbitrary order.

However, if you connect several functions, you may see them called in the order they were connected, but don't rely on this. Never. I mean it.

Connecting a class's method

OK, being able to connect global functions is fine, but you would prefer to connect methods in classes, wouldn't you ? Here's a sample program :

01: #include <iostream>
02: #include <slotsig/slotsig_0.h>
03: void f()
04: { std::cout << "::f()\n" ; }
05: class C : public SlotSig::SlotsSetBase
06: {
07:   public:
08:     C() : SlotSig::SlotsSetBase()
09:     { std::cout << "::C instance at " << this << "\n" ; }
10:     void m1()
11:     { std::cout << "::C[" << this << "]::m1()\n" ; }
12:     void m2()
13:     { std::cout << "::C[" << this << "]::m2()\n" ; }
14: } ;
15: int main (int argc, char* argv[])
16: {
17:   SlotSig::Sig0<void> sig ;
18:   C c1 ;
19:   C c2 ;
20:   sig.connect(f) ;
21:   sig.connect(&c1, &C::m1) ;
22:   sig.connect(&c1, &C::m2) ;
23:   sig.connect(&c2, &C::m2) ;
24:   sig.run() ;
25:   return 0 ;
26: }

(code here)

Here I define a class named C, which inherits from the class SlotSig::SlotsSetBase (lines 5-14). Every class which will contain methods that will be connected to signals should inherits from this class. This isn't absolutely needed, but if you don't, problems may arise if you delete a class's instance to which a signal is connected (more on this later). This class contains two methods, m1() and m2(), both of the «right» type (no parameters, and returning void). The constructor prints the instance's memory adress, to help to track it later.

Two instances of C are created, lignes 18 and 19. Then the methods are connected : m1() and m2() related to c1, and only m2() related to c2. To connect a class's method, you need to give to connect() a pointer to a class's instance, then the pointer-to-member of the method you want to connect. Look at the syntax used, lines 21-23.

The expected output of this program should be something like this (of course, instances' addresses won't be the same for you) :

::C instance at 0xbffffb50
::C instance at 0xbffffb40
::f()
::C[0xbffffb50]::m1()
::C[0xbffffb50]::m2()
::C[0xbffffb40]::m2()

You can see that the method m2() is called twice, but once in c1's context, and once in c2's context.

Now you can try something : connect again something that has already been connected, either the global function f() or a class's method. You'll notice the function or method is called only once (depending of the class's instance if it's a method). This is another very important feature of this signals/slots library :

a global function or a pair [class's instance, class's method] connected more than one time is called only once.

However, this default behaviour can be changed, please have a look at the more advanced documentation.

Disconnecting

So we can call global functions and/or classes methods by connecting them to a signal. But sometimes you may want to break the connection, because you no longer want a function to be called : this is what we call disconnection. The following example program will show you how hardly this is achieved :

01: #include <iostream>
02: #include <slotsig/slotsig_0.h>
03: void f()
04: { std::cout << "::f()\n" ; }
05: class C : public SlotSig::SlotsSetBase
06: {
07:   public:
08:     C() : SlotSig::SlotsSetBase()
09:     { std::cout << "::C instance at " << this << "\n" ; }
10:     void m1()
11:     { std::cout << "::C[" << this << "]::m1()\n" ; }
12:     void m2()
13:     { std::cout << "::C[" << this << "]::m2()\n" ; }
14: } ;
15: int main (int argc, char* argv[])
16: {
17:   SlotSig::Sig0<void> sig ;
18:   C* c1 = new C ;
19:   C* c2 = new C ;
20:   sig.connect(f) ;
21:   sig.connect(c1, &C::m1) ;
22:   sig.connect(c1, &C::m2) ;
23:   sig.connect(c2, &C::m2) ;
24:   sig.run() ;
25:   std::cout << "Disconnecting f and c1::m2...\n" ;
26:   sig.disconnect(f) ;
27:   sig.disconnect(c1, &C::m2) ;
28:   sig.run() ;
29:   std::cout << "Deleting c2...\n" ;
30:   delete c2 ;
31:   sig.run() ;
32:   delete c1 ;
33:   return 0 ;
34: }

(code here)

Note that this time, the two C's instances are created dynamically. Lines 26 and 27 show you how to disconnect something connected : copy-paste the line containing the call to connect() (lines 20 and 22, for example), and insert dis just before connect : you get it. The syntax used to disconnect is a perfect mirror of the syntax used to connect, just replace connect() by disconnect(). I hope this is simple enough.

When run up to line 28, this program will output something like this :

::C instance at 0x8054cf0
::C instance at 0x8054d08
::f()
::C[0x8054cf0]::m1()
::C[0x8054cf0]::m2()
::C[0x8054d08]::m2()
Disconnecting f and c1::m2...
::C[0x8054cf0]::m1()
::C[0x8054d08]::m2()

So the two disconnected slots are not called any more.

Now see the line 30 : we're deleting c2, an instance to which a signal is connected – i.e. an instance which contains a method that shall be called when a signal is emitted. You can guess that, if we emit the signal, and if this signal tries to call the method in the context of an instance that has been destroyed, the program will crash. However, the program doesn't crash, but ends with these two lines :

Deleting c2...
::C[0x8054cf0]::m1()

The program didn't crash because C is a subclass of SlotSig::SlotsSetBase. When deleting an instance of (a subclass of) SlotSig::SlotsSetBase, every signal that contains a connection to a method of this instance is notified of its deletion. And this notification will cause an automatic disconnection, whenever it's needed. If the class doesn't inherits from SlotSig::SlotsSetBase, the program will crash, unless you disconnect everything yourself. This is why every class containing slots (i.e. methods that will be connected to signals) should really inherits from SlotSig::SlotsSetBase.

Of course, you can also safely create a signal using operator new, connect things to it, then destroy it later : everything should be gracefully and automagically done to ensure the overall consistency.

Sending parameters

Until now, we connected functions or methods that don't receive any parameter. Sooner than later you'll want to pass some values to your slots. In fact it's quite straightforward, just use the correct signal type :

01: #include <iostream>
02: #include <slotsig/slotsig_1.h>
03: void f(int i)
04: { std::cout << "::f(" << i << ")\n" ; }
05: class C : public SlotSig::SlotsSetBase
06: {
07:   public:
08:     C() : SlotSig::SlotsSetBase()
09:     { std::cout << "::C instance at " << this << "\n" ; }
10:     void m(int i)
11:     { std::cout << "::C[" << this << "]::m(" << i << ")\n" ; }
12: } ;
13: int main (int argc, char* argv[])
14: {
15:   SlotSig::Sig1<void, int> sig ;
16:   C c ;
17:   sig.connect(f) ;
18:   sig.connect(&c, &C::m) ;
19:   std::cout << "Emitting 1...\n" ;
20:   sig.run(1) ;
21:   std::cout << "Emitting 77...\n" ;
22:   sig.run(77) ;
23:   return 0 ;
24: }

(code here)

Here I want to use slots that receive an integer as unique parameter. We can't use anymore the Sig0<> type, but rather the Sig1<> type, declared in <slotsig_1.h>, included line 2. This signal type allows us to define signals able to send one parameter to the connected slots. It must be given the type of this parameter, as well as the now usual return type of the slots. So to define a signal type that will be connected to slots returning void, and receiving an int as unique parameter, we must write SlotSig::Sig1<void, int>. See line 15.

Connection (and disconnection) doesn't change : lines 17 and 18 show you we use precisely the same syntax as before. The change is when invoking the run() method : now we need to give it the parameter we want to give to our slots. Lines 20 and 22 are examples. The expected output of the previous program is :

::C instance at 0xbffffb50
Emitting 1...
::f(1)
::C[0xbffffb50]::m(1)
Emitting 77...
::f(77)
::C[0xbffffb50]::m(77)

As you can see, each slot receive the parameter given to run().

If you need to send two parameters, use the type SlotSig::Sig2<> declared in <slotsig_2.h>. Create a signal by listing the parameters' types after the return type. For example, to create a signal able to connect to slots returning void, and accepting an integer and a float as parameters, simply write :

SlotSig::Sig2<void, int, float> my_signal ;

By default, you can go up to 16 parameters. See the advanced documentation to extend the library if you need more.

Using the return value

Maybe you're tired of always writing void as return type. Maybe you want your slots to return something, and if possible to get it back. This is perfectly possible, using what I call an accumulator. It can be either a global function, or a functor (a class containing an operator()() method), which takes a unique parameter of the right type. Here's the example program :

01: #include <iostream>
02: #include <string>
03: #include <slotsig/slotsig_1.h>
04: std::string f(int i)
05: { char s[100] = {'\0',} ;
06:   sprintf(s, "%d", i) ;
07:   return s ;
08: }
09: struct C : public SlotSig::SlotsSetBase
10: {
11:   C() : SlotSig::SlotsSetBase() { }
12:   std::string m(int i)
13:   { return f(2*i) ; }
14: } ;
15: void f_accum(const std::string& s)
16: { std::cout << "::f_accum(\"" << s << "\")\n" ; }
17: struct C_Accum
18: {
19:   C_Accum (const std::string& s) : str(s) { }
20:   void operator() (const std::string& s)
21:   { str += " " ;
22:     str += s ;
23:   }
24:   std::string str ;
25: } ;
26: int main (int argc, char* argv[])
27: {
28:   SlotSig::Sig1<std::string, int> sig ;
29:   C c ;
30:   sig.connect(f) ;
31:   sig.connect(&c, &C::m) ;
32:   std::cout << "Emitting 1 using f_accum...\n" ;
33:   sig.run(f_accum, 1) ;
34:   std::cout << "Emitting 3 using C_Accum...\n" ;
35:   C_Accum accum("concat =") ;
36:   sig.run(accum, 3) ;
37:   std::cout << "accum.str = \"" << accum.str << "\"\n" ;
38:   return 0 ;
39: }

(code here)

This time I use a signal sending an integer, to connect slots that return a string. Lines 4 and 12 are such slots (they simply converts the integer to a string). Line 28 defines such a signal's instance. Connection (and disconnection) doesn't change, see lines 30 and 31.

I define a first accumulator as a global function, lines 15-16. It receive a string as unique parameter (the return type of our slots). Well, it's not quite exactly the same type, but here the implicit cast is ok. This function simply prints the received string.

Then a second accumulator, as a class named C_Accum (lines 17-25). This class defines a method operator()(), which receive also a string as unique parameter. This method will concatenate the received string to a string stored as data member of the class, str (line 24), which can be initialized by the constructor (line 19).

To use the accumulator, just give it as first parameter to the run() method (lines 33 and 36). It's always the first parameter, whatever is the number of parameters wanted by the slots. When the signal is emitted, the accumulator will be called each time a slot is called.

The output of this program is :

Emitting 1 using f_accum...
::f_accum("1")
::f_accum("2")
Emitting 3 using C_Accum...
accum.str = "concat = 3 6"

That's all ! The accumulator is quite similar to what you would give as last parameter to the standard algorithm std::for_each().

Conditionnal emitting

Under some circumstances, you don't want all slots to be called. That is, stop the slots calling as soon as some of them return a given value. Think for example of the signal emitted when the user require to quit a running application. Various slots can be called, each of them asking some sort of confirmation : «Are you sure ?» «Do you want to save this ?» «Do you want to save that ?» And so on. If the user answer «no» to the first question, it's useless to ask the others. In such a case, we want to stop the signal while it is emitting.

You can do that using a predicate. It's used in a similar manner than the accumulator previously seen. The code :

01: #include <iostream>
02: #include <string>
03: #include <slotsig/slotsig_0.h>
04: int slot_1()
05: {
06:   std::cout << "This is ::slot_1 !\n" ;
07:   return 1 ;
08: }
09: int slot_2()
10: {
11:   std::cout << "This is ::slot_2 !\n" ;
12:   return 3 ;
13: }
14: int slot_3()
15: {
16:   std::cout << "This is ::slot_3 !\n" ;
17:   return 5 ;
18: }
19: 
20: bool my_pred(int i)
21: {
22:   return ((i % 3) != 0) ;
23: }
24: 
25: int main (int argc, char* argv[])
26: {
27:   SlotSig::Sig0<int> sig ;
28:   sig.connect(slot_1) ;
29:   sig.connect(slot_2) ;
30:   sig.connect(slot_3) ;
31:   sig.run_if(my_pred) ;
32:   return 0 ;
33: }

(code here)

Here's the output :

This is slot_1 !
This is slot_2 !

Only two slots where called, but we connected three...

Look at line 31 : we didn't use run(), but rather run_if(). The first parameter (there are more if you give a value to be passed to slots) is a function (or functor) which takes a unique parameter which type is the one returned by the slots – the same as the accumulator. This function should return a boolean value, or something that can be casted to a boolean value. As soon as this function returns false, the signal is stopped : no more slots are called.

Here the given function, my_pred(), returns false as soon as it receive a number which can be divided by 3 : slot_2() returns such a number, so the signal stops.

Remember, the slots are called in arbitrary order. The method also implies that at least one slot is called.

Signals chaining

What I call « chaining a signal » is, in fact, connecting a signal S1 to a signal S2, as if S1 was a slot. Look at this example :

01: #include <iostream>
02: #include <slotsig/slotsig_1.h>
03: 
04: int slot_1(double d)
05: { 
06:   std::cout << "::slot_1(" << d << ") -> " << (int)d << "\n" ;
07:   return (int)d ;
08: }
09: 
10: int slot_2(double d)
11: {
12:   std::cout << "::slot_2(" << d << ") -> " << (int)(2*d) << "\n" ;
13:   return (int)(2*d) ;
14: }
15: 
16: int sum = 0 ;
17: void accum(int a)
18: { sum += a ; }
19: 
20: bool pred(int a)
21: { return ((a%3) != 0) ; }
22: 
23: int main (int argc, char* argv[])
24: {
25:   SlotSig::Sig1<int, double> sig_1 ;
26:   SlotSig::Sig1<int, double> sig_2 ;
27:   
28:   sig_1.connect(slot_1) ;
29:   sig_2.connect(slot_2) ;
30:   
31:   sig_1.connect(&sig_2) ;
32:   
33:   std::cout << "Running sig_1...\n" ;
34:   sig_1.run(3.14) ;
35:   std::cout << "Running sig_1 with accumulator...\n" ;
36:   sig_1.run(accum, 3.14) ;
37:   std::cout << "Sum = " << sum << "\n" ;
38:   std::cout << "Running sig_1 with predicate...\n" ;
39:   sig_1.run_if(pred, 3.14) ;
40:   
41:   return 0 ;
42: }

(code here)

The output :

Running sig_1...
::slot_2(3.14) -> 6
::slot_1(3.14) -> 3
Running sig_1 with accumulator...
::slot_2(3.14) -> 6
::slot_1(3.14) -> 3
Sum = 9
Running sig_1 with predicate...
::slot_2(3.14) -> 6

As you can see, the chained signal sig_2 is called before the slots of sig_1 – but again, don't rely on this. And if a chained signal, when run with a predicate as it is done ligne 39, is interrupted by this predicate, then the « enclosing » signal is itself interrupted. In fact, line 39 is merely equivalent to :

if ( sig_2.run_if(pred, 3.14) == NoError )
  sig_1.run_if(pred, 3.14)

And it's not so far from what is actually done in SlotSig's code.


Last update 2004-06-10