Content :
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.
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.
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.
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.
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.
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()
.
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.
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