Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Type-safe Signals and Slots in C++: Part 2

0.00/5 (No votes)
13 Feb 2008 1  
Implements a type-safe signal / slot or event / delegate system in C++

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?

SignalSlot-Scheme.gif
  • 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()  // constructor
    {
        m_Slot.AssignFunction(this, OnSlotEvent);
    }

private:
    void OnSlotEvent(char* Text) // callback function
    {
        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()  // constructor
    {
        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 <void, char*> with a slot of the type <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:

SignalSlot-CalendarShot.gif

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.

SignalSlot-CalendarCtrl.gif

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.

SignalSlot-Combine.gif

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 enums 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);  // insert new slot at the end
    MySignal.Connect(NewSlot, false); // insert new slot at the top

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:

SignalSlot-Reentrance.gif

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")
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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here