Features
These classes add a highly comfortable signal / slot system to your projects.
- A signal (event) fired anywhere in any class of your code can be received by slots (delegates) in any class of your code.
- You can connect as many signals to a slot and as many slots to a signal as you like.
- You can pass 0 to 5 arguments of any type to a signal and you can combine the multiple return values from the slots into one single return value given back to the caller (if the return type is not
void
). - If a class is destroyed, all connected signals or slots are automatically disconnected.
- Signals are protected against reentrance.
- It is optimized for high speed.
- The compiled code size is less than 1 KB, no extra libraries required.
- Some additional extra features are provided.
- Platform independent: running on Windows, Linux, Mac, etc...
- Tested on Visual Studio 6.0, 7.0, 7.1 and 8.0 (= Visual Studio 6, up to .NET 2005)
- New in version 3.0 (Oct 2007): support for callbacks to static functions and to functions in virtually derived classes; the
Combine
function must not be static anymore.
Introduction
This article is based on Part 1 (Type-safe C++ Callbacks), but it is not necessary to read Part 1 before reading this article. Signals and slots are callbacks with enhanced features. To use signals and slots, simply copy the files SignalSlot.h, Callback.h and PreProcessor.h to your project and #include "SignalSlot.h"
.
How Does it Work?
- Any class can have as many slots and as many signals as you like.
- Any signal can connect to as many slots and vice versa as you like.
- If signals and slots are public properties, any class can connect to them whenever they want.
- Every class can disconnect its slot or signal at any time when it is not interested in events anymore.
- If a class is destroyed, it automatically disconnects all of its signals and slots. If, in the above example, class Y is destroyed, it disconnects from Slot A in Class X and from Signal 1 in Class X and Z.
Basic Features
Creating a Slot
Every slot needs a function assigned to it that will be called back when an event (=signal) arrives.
class cWorker
{
public:
cSlot <cWorker, void, char*> m_Slot;
cWorker() {
m_Slot.AssignFunction(this, OnSlotEvent);
}
private:
void OnSlotEvent(char* Text) {
printf("cWorker::OnSlotEvent: %s", Text);
}
};
The first type (cWorker
) given to cSlot <...>
is the class that contains the callback function. The second type (void
) is the return type of the callback function. Then follow the arguments for the callback function (char*
). You can use callback functions taking 0, 1, 2, 3, 4 or 5 arguments. If you need more than 5 arguments, you have to expand the callback.h and SignalSlot.h files on your own. However, I recommend passing more than 5 arguments in a structure instead, so the code becomes more readable.
Creating and Connecting a Signal
class cMaster
{
cWorker m_Worker;
cSignal <void, char*> m_Signal;
cMaster() {
m_Signal.Connect(m_Worker.m_Slot);
}
};
The types given to cSignal <...>
are the same as for cSlot <...>
except that the first one (cWorker
) is missing. It doesn't matter if you connect a signal to a slot or vice versa, so the following lines are identical:
m_Signal.Connect(m_Worker.m_Slot);
m_Worker.m_Slot.Connect(m_Signal);
If they are already connected, further calls to Connect()
are ignored. As signals and slots are typesafe, you cannot connect them if the type differs. For example if you would try to connect the above signal of the type <<code>void
, char*
> with a slot of the type <<code>int
, string
, string
, string
, double
, char
>, the result would be a compilation error.
Firing the Signal
The following line fires the signal:
m_Signal.Fire("Now firing....!");
The signal fires all connected slots and then every slot calls its callback function. The result would be the following output from printf()
:
cWorker::OnSlotEvent: Now firing....! |
Usage Example
I wrote my own calendar GUI control for my DesktopOrganizer PTBSync (Download) which looks like this:
The weekday names and month names change immediately when the user switches the system language. The colors of the control adapt immediately when the user modifies the Windows colors in Control Panel. To achieve this, I have to catch the Windows messages WM_SYSCOLORCHANGE
and WM_SETTINGCHANGE
.
However, if you wait inside the control to receive one of these messages, you will never see them because Windows only sends them to the top level windows. This means they will only arrive in the main window of the application (if you don't use MFC).
I use the signal / slot system to notify some controls (like the calendar control) from changes of system settings. The calendar control implements a slot to receive notifications. The corresponding signal is fired in the main window where the Windows messages arrive.
Now I hear you ask, "Why not simply forward the Windows messages to the Calendar control?"
Advantages of signal / slot solution:
- For every new control that wants to be notified, you don't have to modify the code in
MainWindow::WindowProc()
. The new control simply calls MainWindow.SignalXYZ.Connect(MySlot)
in its constructor. - On destruction of the new control, it will be disconnected automatically from the signal.
- You can even notify classes that don't have any windows at all.
- If you put the signal into a globally available singleton class, every class can connect to it whenever it wants.
Advanced Features
Managing Return Values
In the above example, the slots have the return type void
. However, let's say your slots return an int
and you want i_MySignal.Fire(...)
to return the sum of the return values of all callback functions that have been called when firing the signal.
That's no problem! What you need is a Combine
function, which sums the return values of the slots. For the above example, you would write:
static bool Add(int *Result, int NewValue)
{
(*Result) += NewValue;
return true;
}
MySignal.SetCombineFunction(MAKE_COMBINE_S(&MyClass::Add, int), 0);
The macro MAKE_COMBINE_S(Function, ReturnType)
creates a callback to a static Combine
function. For member functions, use the macro MAKE_COMBINE_M(Class, Instance, Function, ReturnType)
. The second argument of Add()
is the result of the latest call to cSlot::Fire()
.
The first argument of Add()
is a pointer to the current sum of the previous calls. The second argument to SetCombineFunction()
, 0
, is an initialization value which is passed with the very first call to Add()
. Why the Combine
function returns true
will be explained later. In the above example, the calls to Add()
would look like this:
*Result (before) | NewValue | *Result (after) | Note |
0 | 3 | 3 | after calling the CWorker slot |
3 | 11 | 14 | after calling the CMyPort slot |
14 | 7 | 21 | after calling the CMyParser slot |
If your slots would return a string
instead of int
and you want i_Signal.Fire(..)
to return the concatenation of all these strings separated by comma, you could write:
static bool Concat(string *Result, string NewValue)
{
if (Result->length()) Result->append(", ");
Result->append(NewValue);
return true;
}
MySignal.SetCombineFunction(MAKE_COMBINE_S(&MyClass::Concat, string),
"Returned from MySignal: ");
.....
string Result = MySignal.Fire(..);
You are absolutely free in writing your specialized Combine
function. For example, you could return a bitmask from the slots and OR
the bits together in the Combine
function to see which operations have been executed successfully. Alternatively, you could use enum
s to return a status code.
Slot Priority
You can have slots of different priority. The ones with higher priority are called first. If one of them has handled the event successfully, it can stop further processing. You can specify in which order a signal calls the connected slots by the order of connecting them. If you write:
MySignal.Connect(Slot1);
MySignal.Connect(Slot2);
MySignal.Connect(Slot3);
MySignal.Fire()
will call Slot1
first and then Slot2
and, finally, Slot3
. But what if you want to connect a new slot later? You can pass an optional second argument to cSignal::Connect(cSlot &Slot, bool Append=true)
.
MySignal.Connect(NewSlot, true); MySignal.Connect(NewSlot, false);
Aborting Slot Firing
The Combine
function offers a second feature: if your Combine
function returns false
, no more slots will be fired after the current one. This is useful if your slots are specialized to handle only a special type of event. Then the first slot that handled the event successfully can stop processing. For example, your slots return an int
and you want no more slots to be called if the sum is greater than 50.
static bool Add(int *Result, int NewValue)
{
(*Result) += NewValue;
return (*Result <= 50);
}
Reentrance Protection
In a very complex project, it is nearly sure that someday you will build a reentrant loop like the following:
Signals and slots are protected against reentrance. A call to cSignal::Fire()
or cSlot::Fire()
is ignored if it is currently processing a previous call. For this case, you can define a return value which is returned by Fire()
on reentrance. This default value has to be passed to the constructor.
Constructor
cSlot <cClass, tRet [,tArg1,,,tArg5]> Slot(tRet DefaultValue = (tRet) 0)
cSignal <tRet [,tArg1,,,tArg5]> Signal(tRet DefaultValue = (tRet) 0, ....)
If you don't specify a value for DefaultValue
, it is set to zero. Attention: not all return types can be set to zero! For example, if tRet
is string
, the constructor tries to set string DefaultValue=(string)0
which is not possible. Avoid this by writing, for example:
cSlot <cMyClass, string, tArg1, tArg2...> MySlot("Error");
If this slot is called reentrant, Fire()
will return a string with "Error." The return type void
is a special case. Although the constructor sets void DefaultValue = (void) 0
, this does not cause a compiler error because I use a lot of tricks in SignalSlot.h that I will not explain here.
Timeout
For every signal or slot, you can set a timeout in which further events will be ignored.
Signal.SetMinInterval(2000, false)
This sets the timeout to 2 seconds. After firing the signal, there has to be a pause of at least 2 seconds until the signal can be fired again. In the meantime, Fire()
will return the default Timeout
value of false
.
Defaults
Here's a summary of the 3 default return values which have already been explained above:
Type | Slot | Signal | Set with | Usage |
Combine default | - | X | SetCombineFunction() | passed to Combine function with the very first call |
Reentrance default | X | X | Constructor | returned from Fire() on reentrance |
Timeout default | X | X | SetMinInterval() | returned from Fire() within timeout period |
Debugging
In complex projects, you will lose the overview of which signal is connected to which slot. For that, cSignal
and cSlot
offer a debugging function:
bool GetConnectedList(char* Buf, int BufLen)
It outputs all the slots connected to a signal or all the signals connected to a slot by name. If the buffer is too small, GetConnectedList()
returns false
. To use this functionality, you have to give unique names to your slots and signals. You can name them with an optional argument to the constructor of the Signal
and AssignFunction()
of the Slot
:
cSignal <tRet [,tArg1,,,tArg5]> Signal(tRet DefaultValue=0, char* Name="NoName")
cSlot::AssignFunction(Instance, Function, char* Name="NoName")
Then call GetConnectedList
:
char Buffer[5000];
MainSignal.GetConnectedList(Buffer, sizeof(Buffer));
TRACE(Buffer);
This will output, for example:
Signal MainSignal is connected to: PrinterSlot, DataSlot, MessageSlot |
The Interface, Overview
cSlot | cSignal |
Basic Features
|
Constructor:
cSlot <cClass, tRet [,tArg1,,,tArg5]> Slot(tRet DefaultValue=0) | Constructor:
cSignal <tRet [,tArg1,,,tArg5]> Signal(tRet DefaultValue=0, char* Name="NoName") |
void AssignFunction(Instance, Function, char* Name="NoName")<br />void AssignFunction(Function, char* Name="NoName") | - |
void Connect(&Signal, bool Append=true) | void Connect(&Slot, bool Append=true) |
void Disconnect(&Signal) | void Disconnect(&Slot) |
void DisconnectAll() | void DisconnectAll() |
tRet Fire(tArg1,,,tArg5) | tRet Fire(tArg1,,,tArg5) |
Advanced Features
|
void SetMinInterval(unsigned int Time, tRet DefaultValue) | void SetMinInterval(unsigned int Time, tRet DefaultValue) |
- | void SetCombineFunction(Function, tRet DefaultValue) |
For Debugging
|
int GetConnectedCount() | int GetConnectedCount() |
bool GetConnectedList(char* Buf, int BufLen) | bool GetConnectedList(char* Buf, int BufLen) |
To see how all the stuff works together, download the sample code! It also demonstrates the usage of Timeout
and the Combine
function.
Boost, QT
Boost and QT also offer signal / slot functionality (see Part 1 of the article series). However, the signal slot system by ElmueSoft described in this article has the great advantage that it is tiny, does not need any big libraries or compiler plugins and it is free! You will not want to miss it if you once got used to it!
P.S. From my homepage, you can download free C++ books in compiled HTML format.
History
- 21 April, 2004 -- Original version posted
- 13 February, 2008 -- First update
- Version 3.0
- Article content and source download updated