Using Signals/Slots

It recently became clear that a number of subtle bugs in Ardour (particularly Ardour3) were caused by the lack of thread safety in libsigc++, a library I've previously spoken of as "the best thing to happen to C++ ever". Alas, my cavalier use of it even in spite of my knowledge that it was not thread safe has now required several days of effort to move the codebase over to a similar library that is thread safe: boost::signals2. This post reviews my experiences while doing this, and compares and contrasts the two libraries from the specific perspective of their use in Ardour.

Syntax, Syntax, Syntax

The first issue when switching was that boost signals have a significantly, but subtly different syntax than their sigc counterparts. A signal whose recipients do not return and get no arguments is signal<void> in sigc++ and signal<void()>. boost::signals2 is using a syntax similar to but not identical to the way you'd declare a function pointer. So a signal whose recipients return int and accept 3 arguments, a float, a string and a Rhino is signal<int(float,std::string,Rhino)>. The sigc++ equivalent would have used commas only: signal<void,int,std::string,Rhino>.

A Slot for Everything and Everything in Its Slot

boost::signals doesn't require the use of any wrapper function comparable to sigc::ptr_fun to connect regular functions or static methods to a signal: aSignal.connect (someFunctionPtr) replaces aSignal.connect (ptr_fun (someFunctionPtr) in sigc++.

However, using member functions is somewhat more complex. Whereas in sigc++, we might write aSignal.connect (mem_fun (someObjectReference, &SomeObject::method)) in boost it seems that the simplest construction is aSignal.connect (bind (&SomeObject::method, SomeObjectPointer)). Note not only the yumilicous argument reversal between boost::bind and sigc::mem_fun, be sure to check out the incredible importance of NOT passing references into bind() calls - by default you will just get the copy constructor for SomeObject called, and the instance on which the method is invoked is a copy of the original. Pass a pointer or use the syntax ref (someObjectReference) to be sure you are using a reference to the original object.

Make it Binding

boost::bind() is fairly cool framework (technically, its part of a different "library" within boost. Its function is similar to sigc::bind, except that you start with the actual object method and object pointer, rather than a slot constructed with sigc::mem_fun() or sigc::ptr_fun().

However, its quite a bit more cumbersome to use for any signal that delivers arguments to its recipients. Whereas sigc++ "understands" the signature of the resulting slot, and notices that it matches that of the signal when making the connection, the boost version (appears to) require that you put explicit placeholders in the bind call to represent the arguments that will be passed to it. This is tedious. You must also be aware of the number arguments that will be passed by the signal if you want to add additional connect-time arguments. Thus, in sigc++, we could have used:

signal<void.int.float> aSignal; // delivers two arguments
void aFunction (int an_int, float a_float , float extra_arg);  // expects 3
aSignal.connect (bind (ptr_fun (aFunction), 3.14159));
      
in boost::signals we have to say:
signal<void(int.float)> aSignal;
void aFunction (int an_int, float a_float , float extra_arg);
aSignal.connect (bind (aFunction), _1, _2, 3.14159));
      
Those underscore-prefixed numbers? Boost-magic placeholders that mean "the 1st argument goes here" and "the 2nd argument goes here". This is incredibly flexible, allowing you to reorder arguments and so forth. However, the need to state them explicitly was very irritating, at least during the porting effort.

Other pitfalls that I ran into a lot when converting to use boost::bind():

  • accidentally passing in a reference to an non-copyable object to the bind, instead of a pointer

Connection Management

One of the very nicest features of sigc++ is the way it automatically takes care of connection management. Suppose you write the following:

struct SignalOwner {
    ...
    signal<void> theSignal;
};

struct SignalReceiver : sigc::trackable {
    ...
    void theMethod;
};

SignalOwner* owner (new SignalOwner);
SignalReceiver* receiver (new SignalReceiver);
...
owner->theSignal.connect (mem_fun (*receiver, &SignalReceiver::theMethod));
...
delete receiver;
owner->theSignal();
      

What happens? We connected the object to the signal, then deleted it, the emitted the signal. Thankfully, sigc++ takes care of all this for us via its sigc::trackable class, which will ensure that the connection to the receiver object is removed when it is destroyed. Just remember to derive anything that connects to a signal from sigc::trackable, and you don't need to worry about this at all.

Of course, every silver lining has a cloud. This connection management is one of main areas of sigc++'s lack of thread safety. Its handle by the signal and the receiver keeping lists to show who is connected, and these lists are not locked against concurrent access. Two threads add a new connection to the same signal at the same time? The list is corrupted. One thread removes a connection while another one is adding one? List corruption. There are many scenarios, hence the move of Ardour's backend to boost::signals2.

Boost doesn't offer a thread-safe replacement for sigc::trackable, because its not possible to do (or at least, the people who worked on the library never figured out a way to do so). Instead, it offers several different approaches to connection management. Ardour is using just one of them: boost::signals2::scoped_connection. We have several approaches to using them, outlined in the style guide

No Need To Hide

A subtle detail of boost::bind() is that it removes the need to replace sigc::hide. If you are connecting a method/function that doesn't want to deal with one or more of the arguments that will be passed by a signal, just leave out the relevant _N placeholder when calling boost::bind().