XL Signal and Slots for C++

Introduction

Signals and slots are used for communication between objects. There are instances when we want some objects to know the changes of a particular object without having to write code for each interested object. This design is common in GUI programming, where another widget needs to be notified implicitly of the change of one widget. Since the widget needs to be usable everywhere, the design should be as generic as possible. The event-generating widget should not need to know how or which widgets are informed of the event.

In older C-based toolkits, this functionality is achieved by using "callbacks" - registering pointers to functions to a processing function. The problem with callbacks is that they are not type safe, prone to crashes, and are strongly tied to the processing function.

Signal and Slots is an implementation of the Observer design pattern made popular by the Qt toolkit. It provides a much safer alternative to callbacks in addition to being built on the principles of object-oriented programming. Qt implemented this feature by dynamically adding extra information to classes during compile time with the help of an external meta object compiler.

XL Signal and Slots achieves the same functionality without the meta compiler by taking advantage of C++ templates. This direct approach is very fast compared to Qt's string-based approach with the added benefit of:

At the same time, the library tries to follow the design of Qt as closely as possible where it:

The library aims to achieve the core signal and slots functionality very well and at the same time be as less complicated as possible. In around a thousand lines of code, the library is capable of being embedded into the application in a very tiny amount of space.

A simple example

Signals are emitted when a particular event occurs. Once a Signal is emitted, all slots connected to it are immediately called. If you are interested in an event, you need to connect a signal to a slot. Slots can be any standard member function of a class derived from XL::Object. Its access rights determine which signals can connect to it. The argument of a slot must match that of the signal it intends to be connected to. Similar to Qt's Slots, all slots must return only void.

A class that has a signal declaration might be defined as:

class Counter
{
public:
	XL::Signal< int > valueChanged;

private:
	void secondsElapse(int sec);
};

A class that has a slot interested in the above signal might be defined as:

class Clock: public XL::Object
{
public:
	void displaySeconds(int s);
};

If a Clock object wants to be automatically informed by a Counter component to display the time, the Clock's displaySeconds() method must be connected to the Counter's valueChanged signal:

Counter counter;
Clock clock;
counter.valueChanged.connect(&clock, &Clock::displaySeconds);

Signals have a connect method that takes a pointer of an Object and its corresponding member function in standard pointer to member function syntax. An optional SLOT macro is also provided to clarify your code. When using the SLOT macro, you no longer need to use the pointer to member function syntax when specifying a slot.

counter.valueChanged.connect(SLOT(&clock, Clock::displaySeconds));

Creating Signals

You can create your own signals. Any number of signals can be connected to a single slot and vice-versa. A signal can also be directly connected to another signal of the same type. The second signal will be immediately emitted whenever the first is emitted.

The correct syntax in creating a Signal is:

XL::Signal<TypeArg1, TypeArg2, ..., TypeArgN> signalobject;

The type of arguments is specified in the template parameter list of the Signal. Signals and slots can take up to seven arguments of any type. You can connect any slot that matches the type of the signal. The library ensures that the right slot will be called with the signal's parameters at the right time. You can even connect slots that have a shorter argument signature than the signal and it will ignore the extra arguments.

class Sender
{
public:
	XL::Signal<int, const MyType&, const std::string& > signalobject;
};

class Receiver
{
public:
	void function3(int t, const MyType& mt, const std::string &s);
	void function2(int t, const MyType& mt);
	void function1(int t);
	void function0();

	void otherfunc(const std::string &s);
};

Sender s;
Receiver r;
s.signaloject.connect(SLOT(&r, Receiver::function3)); // exact match of slot.
s.signaloject.connect(SLOT(&r, Receiver::function2)); // okay. ignore extra arguments
s.signaloject.connect(SLOT(&r, Receiver::function1)); // okay. ignore extra arguments
s.signaloject.connect(SLOT(&r, Receiver::function0)); // okay. ignore extra arguments

s.signaloject.connect(SLOT(&r, Receiver::otherfunc)); // compiler error! type mismatch

Triggering a Signal

A signal will trigger all signal and slots connected to it immediately once it is emitted. There are two ways to emit a Signal. You can either use Object::emit() or Signal::emit(). The latter method is used on freestanding signals. Here's how we emit the valueChanges signal on our initial Clock and Counter example:

void Counter::secondsElapse(int sec)
{
	valueChanged.emit(sec);
}

Use Object::emit() to let connecting slots know which sender object initiated the signal. If you need to use this feature, make sure that the class holding the Signal is also derived from XL::Object. You can then use Object::sender() inside the slot to obtain a pointer to the object that sent the signal.

class Counter: public XL::Object
{
public:
	XL::Signal< int > valueChanged;

private:
	void secondsElapse(int sec) {
		emit(valueChanged(sec));
	}
};

Counter counter;
Clock clock;
counter.valueChanged.connect(SLOT(&clock, Clock::displaySeconds));

Clock::displaySeconds(int s)
{
	Counter* c = (Counter *) sender();
	c->someMethod();
}

The Signal's emission may be deferred at a later time. To do this, you need to specify false to the autoexec argument of the Signal::emit() method. This is set to true by default. If autoexec is set to false, Signal::emit() returns a BlockedSignal object that pauses the triggering of the stored slots until it is explictly triggered later by BlockedSignal::play().

void Counter::secondsElapse(int sec)
{
	BlockedSignal< int > b = valueChanged.emit(sec,false); // does not trigger the slots
	b.play(); // trigger the slots now.
}

Object Trees and Object Ownership

You can optionally organize XL Objects in object trees. Though this feature is not required for Signal and Slots functionality, it comes as an added bonus when you derive your objects from XL::Object. One of the biggest advantage when using Object trees in your application is that you no longer need to manually manage the lifetime cycle of your objects. When an XL::Object is created with another object as parent, it's added to the parent's children list, and is deleted when the parent is. This approach is ideally suited when mixing GUI objects with dynamically allocated Objects representing extra information. Child objects may also be deleted manually, they will remove themselves from their parent in the process.

For example, you can define an XL::Object-derived class with dynamically allocated members that are themselves derived from XL::Object. You no longer need to worry about not deleting the objects at a later time since XL takes care of it all for us.

class MyObject : public XL::Object
{
public:
	MyObject(XL::Object* parent):
		XL::Object(parent)
	{
		pb = new PushButton(this,..);
		sq = new SqlDatabase(this,...);
	}	
	
	~MyObject()
	{
		// no need to delete sub objects
	}
private:
		
	PushButton* pb;
	SqlDatabase* sq;	
};

Where to get XLObject

Latest version is available via SVN
 svn co https://xlobject.svn.sourceforge.net/svnroot/xlobject xlobject
Download old releases via sourceforge download .

Cool software using XLObject as a foundation:

Qt is a registered trademark of Trolltech in Norway and worldwide.

2003 - 2006 Abdiel Janulgue (xynopsis [at] yahoo dot com)

sf logo