Webmaster  |  Imprint 
Platinum
Platinum C++ Framework
Main  |  License  |  Documentation  |  Download  |  Support 

Signals and Delegates

Introduction

Callback mechanisms for event handling have become ubiqitous in todays application frameworks. Older examples are the use of function pointers as callbacks or the message maps found in the MFC toolkit. More modern approaches include the .NET delegates and so-called signal-slot techniques. When signals and slots are used, objects can communicate with each other by connecting a signal of one object to the slot of another object. In most cases connection management features are built-in so that an object closes all it connections automatically when it gets destroyed. After the connection has been established, each time a signal is send, all registered slots are being called. Connecting signals to slots is type-safe i.e. a signal can only be connected to a slot that matches the signal's signature. At the same time it allows a great deal of flexibility (loose coupling), since the caller has no intimite knowledge of the callee. A simple but real example might look like this:

int main()
{
    Pt::System::Timer timer;
    timer.start(1000);

    Pt::System::Application app;
    app.add(timer);
    connect( timer.timeout, app, &Pt::System::Application::exit);

    return app.run();
}

The application will simply quit when the timer expires after 1000 ms. The Application object is the callee and the Timer is the caller, which has a signal called timeout. In this case Application::exit is a regular member function.

Signals

Pt::Signal is a class template and can be instantiated like any other object. Usually, signals are members of objects and are being sent when the object state changes. The template parameter list of the Pt::Signal class template determnes the signature of the signal. If a signal does not have any arguments the parameter list is left empty:

Pt::Signal<> sig0;                        // Signal without arguments
Pt::Signal<int> sig1;                     // Signal with one argument
Pt::Signal<int, int> sig2; // Signal with two arguments

Connecting Signals

A signal can be connected to a slot if the signatures are compatible. The function connect is overloaded for various types and provides the easiest way to form a connection. One important feature of Pt::Signal is that the return value of a slot is ignored and therefore a slot is compatible to a signal no matter what type it returns. The following code example shows how a signal is connected to a free function and a member function:

class Callee : public Pt::Connectable
{
public:
    void slot()
    { printf("Callee::slot() called\n"); }
};

void slot()
{ printf("slot() called.\n"); }

int main()
{
    Pt::Signal<> signal;
    Callee calee;

    connect( signal, slot );
    connect( signal, callee, &Callee::slot);

    return 0;
}

The first argument to connect is aleays the signal object. When a signal is connected to a free function the second parameter to the connect call is a function pointer. When a signal is connected to a member function of an object, the second argument is an object instance and the third one the member function pointer. Signals can only be connected to objects that derive from Pt::Connectable to ensure that all connections are closed when the object goes out of scope and no dangling connections are left.

The connecting functions return Connection objects, which can be used to disconnect signals from slots manually. The following code illustrates this:

void slot()
{ printf("slot() called.\n"); }

int main()
{
    Pt::Signal<> signal;
    Connection c = connect( signal, slot );
    c.valid() // returns true

    c.close();
    c.valid() // returns false now

    return 0;
}

A connection is reference counted and can not be duplicated as such, but always refers to the same shared connection data. If one peer of a connection is destroyed or connection::close is called, the connection becomes invalid. A connection can also become invalid if one of the connection peers rejects being connected. Pt::Signal never rejects connections, however other signal types might.

Besides the overloaded versions of connect, Pt::Signal::connect can be used to connect to any matching slot type, and thus Pt::Signal::connect takes a reference to a Pt::BasicSlot. Slots are lightweight proxy-objects and one example is Pt::MethodSlot, which describes a member function slot. This way, signals can be connected to any callable entity.

Sending Signals

Once a connection has been established, signals can be send to invoke the connected slots. This happens by calling Pt::Signal::send with the appropriate arguments, if any.

void tellAge(int age)
{ std::cout << "I am " << age << " years old\n; }

int main ()
{
    Pt::Signal<int> signal;
    connect( signal, tellAge );
    signal.send(26);
    return 0;
}

When the signal is send, the slot is called with the value passed to Signal::send. Nothing will happen if the signal is not connected to any slots. When a signal is sent, the slot is called immediatly and directly and does not depend on an event loop. If multiple slots are connected to a signal, the slots will be called one after another.

Delegates

A Pt::Delegate is an alternative sender type to the Pt::Signal and differs from it in two ways. Firstly, delegates forward the return value of the slot and secondly, delegates can only be connected to one slot at a time.

Connecting Delegates

The syntax for connecting delegates is identical to how signals are connected to slots. However, since a delegate forwards the return value of its slot, not only the arguments passed to the slot must be compatible, but also the return value. Furthermore, when an already connected delegate is connected again, the current connection will be closed and the delegate is connected to its new target.

int slotA()
{ return 5; }

int slotB()
{ return 6; }

int main()
{
    Pt::Delegate<int> delegate;
    connect( delegate, slotA );
    connect( delegate, slotB ); // disconnects from slotA

    return 0;
}

Calling Delegates

There are two possibilities how a Pt::Delegate can call its slot. Pt::Delegate::call will return the return value of the slot. If the delegate is not connected to a slot, an exception is thrown. The second method is through Pt::Delegate::invoke where the return value is ignored, but if the delegate is not connected no exception will be thrown.

int slot()
{ return 5; }

int main()
{
    Pt::Delegate<int> delegate;
    connection conection = connect( delegate, slot );

    int i = delegate.call(); // i is 5 now

    connection.close();
    delegate.invoke() // does not throw
    delegate.call();  // will throw because not connected

    return 0;
}
Copyright © 2003-2007 The Pt Development Team
Pt 1.0