Boost Synapse

This is a short introduction to Boost Synapse. There are also several complete example programs that may be of interest to the beginner.

Defining signals

Defining signals is very simple. For example, to define a mouse_move signal that takes two ints we could use:

typedef struct mouse_move_(*mouse_move)( int x, int y );

The return type (in this case mouse_move_) is ignored when the signal is emitted. Instead, it is used to tell apart different signals that take the same arguments. For example, the button_down signal defined below is different from mouse_move even though they both take two ints:

typedef struct button_down_(*button_down)( int x, int y );

That's it! Now we're ready to emit signals!

Emitting signals

To emit a signal simply call emit passing an emitter object. Any object of any type whatsoever can be used as an emitter. For example, from a member function we could pass this as the emitter:

//Define a button_clicked signal that takes no arguments.
typedef struct button_clicked_(*button_clicked)();

class
my_button
    {
    ....
    void
    emit_button_clicked()
        {
        synapse::emit<button_clicked>(this);
        }
    };

Of course, while the above call is well defined, it will not invoke any functions because we haven't connected anything to the signal yet.

Connecting signals

We have defined and emitted a signal, but to respond to it we need to connect it to a function. This is done using connect, which returns a connection object. For example:

my_button b;
boost::shared_ptr<synapse::connection> c=synapse::connect<button_clicked>(&b,f);

This connects the button_clicked signal from the my_button object b to the function object f, which of course must match the signature of the signal being connected—in this case f must be a function that takes no arguments. Calling emit invokes all function objects that are connected to the specified emitter, in the order they have been connected.

To break the button_clicked connection between b and f, simply let the connection object expire. Optionally connect takes a third weak_ptr<void const> argument. If specified, emit will lock it before invoking the connected function and will not call the function if the weak_ptr has expired. For example this is useful if f binds a member function of an object managed by shared_ptr to ensure that emit does not invoke it if the object has expired:

class
target
    {
    public:
    target(....);
    void do_something();
    };

my_button b;
boost::shared_ptr<target> t=boost::make_shared<target>(....);
boost::shared_ptr<synapse::connection> c=synapse::connect<button_clicked>(&b,
    boost::bind(&target::do_something,t.get()),t);

synapse::emit<button_clicked>(); //invokes t->do_something().
t.reset();
synapse::emit<button_clicked>(); //does not invoke t->do_something().

We have passed t as the third argument to connect, and now the connection retains a weak_ptr to t. This allows Boost Synapse to detect that by the time the second call to emit<button_clicked>() occurs the target has expired, and so t->do_something() will not be invoked—even though the connection object c is still afloat.

Notice that in the call to boost::bind we pass t.get() rather than t. Had we passed t, the function object bind returns would be keeping the target afloat—which is a valid design choice but not what we wanted in this case. Passing a raw poither is safe because when emit calls a connected function, it first locks the weak_ptr passed to connect.

Emitting signals from objects of 3rd-party types

Recall that any object whatsoever can be used as an emitter. This makes it possible to emit non-intrusively even if the emitter object is not built to support signals. For example, a function that processes a file can use the standard FILE pointer as a Boost Synapse emitter to report on its progress:

typedef struct report_progress_(*report_progress)(int);

void
process_file( FILE * f )
    {
    for( int progress=0; !feof(f); )
        {
        ....
        progress += fread(buf,1,nread,f);
        synapse::emit<report_progress>(f,progress);
        }
    }

Outside of process_file the report_progress signal can be connected to some user interface function that updates a progress bar. Using Qt, this could look like this:

if( FILE * f=fopen("file.dat","rb") )
    {
    QProgressBar pb(....);
    auto c=synapse::connect<report_progress>(f,boost::bind(&QProgressBar::setValue,&pb,_1));
    process_file(f);
    }

Notice that process_file is not coupled with QProgressBar: the report_progress signal could be connected to a different function or not connected at all, in which case the call to emit would be a no-op.

For another example of calling emit on an object of a 3rd-party type see "Handling Events from an OS Message Pump".

Interoperability with other signal programming APIs

Boost Synapse can be used to extend the functionality of other signal programming APIs. Suppose we call synapse::connect with an object of type QPushButton:

//Define a Boost Synapse signal.
typedef void QPushButton_clicked_(*QPushButton_clicked)();

....
QPushButton b;
boost::shared_ptr<synapse::connection> c=synapse::connect<QPushButton_clicked>(&b,f);

Now we can call synapse::emit "manually" to make the QPushButton emit our QPushButton_clicked signal (which in this case will call f as long as the connection is alive):

synapse::emit<QPushButton_clicked>(&b);

As is the case with the previous FILE pointer example, due to the non-intrusive nature of Boost Synapse, emitting the QPushButton_clicked signal does not require cooperation from the QPushButton type itself.

Note: This technique can be used to define new types of signals for existing Qt types, which is not supported by the native Qt API (in Qt, to add custom signals to a button we must define a new type that derives from QPushButton, which then must be passed through the proprietary Qt Meta Object Compiler, which is difficult without qmake). For a complete example see "Adding Custom Signals to Qt Objects Without MOCing".

We can also make the QPushButton call emit<QPushButton_clicked> automatically when clicked by connecting its native QPushButton::clicked signal (using the Qt API) to synapse::emit:

QObject::connect(&b,
    &QPushButton::clicked,boost::bind(&synapse::emit<QPushButton_clicked>,&b);

Meta signals

In the above example, even though clicking the QPushButton calls emit<QPushButton_clicked> automatically, the setup was not automatic. This can be improved by using meta signals.

Every time connect<QPushButton_clicked> is called, the global meta::emitter emits the special signal meta::connected<QPushButton_clicked>. A possible handler of this signal may be declared like this:

void handle_QPushButton_clicked_connect( synapse::connection & c, unsigned flags );

The first parameter, c, refers to the connection object created by connect<QPushButton_clicked>. The second parameter is a combination of bits which depends on the circumstances in which the handler is invoked. In particular, the connecting bit is set when the connection object is being created; the meta signal is also emitted just as the connection object is expiring, in which case the connecting bit is not set.

Regardless, the handler may use connection::emitter to access the emitter object passed to connect:

void
handle_QPushButton_clicked_connect( synapse::connection & c, unsigned flags )
    {
    if( flags&synapse::meta::connect_flags::connecting )
        {
        boost::shared_ptr<QPushButton> b=c.emitter<QPushButton>();
        QMetaObject::Connection qc=QObject::connect(b.get(),&QPushButton::clicked,
            boost::bind(&synapse::emit<QPushButton_clicked>,b.get()));
        c.set_user_data(qc);
        }
    else
        QObject::disconnect(*c.get_user_data<QMetaObject::Connection>());
    }

Calling c.emitter<QPushButton> returns the QPushButton object being passed to the call to connect<QPushButton_clicked> which triggered our handler. Next, we use the Qt API to connect the Qt-native QPushButton::clicked signal to synapse::emit, storing the resulting Qt connection object into c using set_user_data (which can be used to store a value of any type in c). When the synapse::connection expires (the else branch), we retrieve the Qt connection from the synapse::connection using get_user_data and pass it to QObject::disconnect.

All that remains is to connect the handle_QPushButton_clicked_connect function to the meta::emitter. This can be done by a simple namespace-scope call to connect placed in the same compilation unit that defines the handler:

auto meta_QPushButton_clicked=synapse::connect<synapse::meta::connected<QPushButton_clicked> >(
    synapse::meta::emitter(),&handle_QPushButton_clicked_connect);

We can now directly pass QPushButton objects to connect to install handlers for QPushButton_clicked to be invoked when buttons are clicked:

QPushButton b1, b2;
    {
    //Make two QPushButton_clicked connections: this also emits the
    //synapse::meta::connected<QPushButton_clicked> signal twice,
    //each time triggering our installed handler which connects the Qt-native
    //signal QPushButton::clicked to synapse::emit<QPushButton_clicked>.
    boost::shared_ptr<connection> c1=synapse::connect<QPushButton_clicked>(&b1,f1);
    boost::shared_ptr<connection> c2=synapse::connect<QPushButton_clicked>(&b2,f2);
    ....
    //Until c1 and c2 expire clicking b1 calls f1 and clicking b2 calls f2.
    ....
    }
//At this point c1 and c2 have expired, emitting meta::connected<QPushButton_clicked>
//twice again, each time triggering our intalled handler to disconnect the Qt-native signal.

This documentation includes two complete example programs that illustrate the use of meta::connected handlers:

Emitter lifetime safety

Because the emitter object passed to connect may have been destroyed by the time it is accessed by a handler of the meta::connected signal, connect can also take the emitter argument by weak_ptr:

boost::shared_ptr<my_button> b(new my_button);
boost::shared_ptr<synapse::connection> c=synapse::connect<button_clicked>(b,f);

In this case the connection retains a copy of the passed weak_ptr which will be locked by a later call to c.emitter<my_button>() to return a shared_ptr to the emitter. If instead connect was given a raw pointer, the shared_ptr returned by c.emitter<my_button>() still points to the emitter object but does not keep it afloat.

Emitter type safety

The connection::emitter function template is type-safe: the type of the emitter object is captured at the time it is passed to connect, and then connection::emitter returns an empty shared_ptr if the type it is instantiated with is incompatible with the captured type.

One complication with connection::emitter is that it is not aware of any possible implicit coversions if the emitter type is part of a class hierarchy. For example:

boost::shared_ptr<QPushButton> b(....);
boost::shared_ptr<synapse::connection> c=synapse::connect<QPushButton_clicked>(b,f);
boost::shared_ptr<QWidget> w=c.emitter<QWidget>(); //fail

We've passed the emitter to connect as a QPushButton, and later—knowing that QWidget is a base class of QPushButton—we want to access it as a QWidget. Currently this is not supported; the only way to deal with this problem is to try different base types:

if( boost::shared_ptr<QPushButton> b=c.emitter<QPushButton>() )
    {
    /* connect was given a QPushButton. */
    }
else if( boost::shared_ptr<QWidget> w=c.emitter<QWidget>() )
    {
    /* connect was given a QWidget. */
    }
else if( boost::shared_ptr<QObject> o=c.emitter<QObject>() )
    {
    /* connect was given a QObject. */
    }
else if( boost::shared_ptr<void const> p=c.emitter<void const>() )
    {
    /* connect was given some other type. */
    }
else
    {
    /* Because any type can be accessed as void const,
        hitting this "else" branch means that the emitter has expired. */
    }

Note however that connection::emitter does handle const-ness correctly. For example, if connect was passed a QPushButton emitter, it can be accessed as QPushButton and as QPushButton const, but if connect was passed a QPushButton const then it can not be accessed as a QPushButton.

Blocking signals

It is possible to block a specific signal for a specific emitter. While the signal is blocked, calls to emit are ignored:

my_button b;
boost::shared_ptr<synapse::connection> c=synapse::connect<button_clicked>(&b,f);
    {
    boost::shared_ptr<synapse::blocker> blk=synapse::block<button_clicked>(&b);
    synapse::emit<button_clicked>(&b); //Doesn't call any handlers, button_clicked is blocked.
    }

//Here blk has expired, unblocking the button_clicked signal for b.
//The following call to emit will call the connected handlers.
synapse::emit<button_clicked>(&b);

In general, a signal will remain blocked for a given emitter until all blocker objects for that signal and that emitter have expired. Note that blocking a signal affects current as well as future connections:

my_button b;
boost::shared_ptr<synapse::blocker> blk=synapse::block<button_clicked>(&b);
boost::shared_ptr<synapse::connection> c=synapse::connect<button_clicked>(&b,f);

synapse::emit<button_clicked>(&b); //Doesn't call any handlers, button_clicked is blocked.

In addition (similarly to connect) there is a meta signal associated with block. When a specific signal for a specific emitter transitions from being unblocked to being blocked, the meta::emitter emits meta::blocked, passing true for its is_blocked argument. The signal is emitted again when the signal becomes unblocked and this time is_blocked is false. One possible use of meta::blocked is to automatically reflect the blocked state of the signal in some user interface; see "Using Meta Signals to Respond to Signals Being Blocked or Unblocked".

Multi-threading support

Please see Interthread Communication Support.

Header-only emit

If no signals are ever connected, emit is a no-op. For this reason, if a user library calls emit but does not call connect, and if the program that links the user library does not call connect either, there is no need to link the Boost Synapse library. As an example, a low level library that emits signals similar to report_progress (see the FILE example earlier on this page) can do so without requiring a client program that doesn't care about them to link Boost Synapse—yet other client programs that do connect and handle the signals will "just work" without needing to recompile the low level library.


See also: Boost Synapse