Difference between revisions of "Using libsigc++ signals"

From Inkscape Wiki
Jump to navigation Jump to search
(Created page with 'This is a very short guide to using libsigc++ signals. They can make complex code with a lot of things to update after each change in some data structure very simple. ==What are...')
 
Line 134: Line 134:
===Accumulators===
===Accumulators===


Accumulator is a small class template that defines a signal's return type and a call function that evaluates the slot list of a signal. (TODO finish his chapter)
Accumulator is a small class template that defines a signal's return type and a call function that evaluates the slot list of a signal. (TODO: finish)
 
WARNING: I have discovered a bug in libsigc++ that makes accumulators severely broken, because their result type cannot be different from the result type of the slots. See [http://bugzilla.gnome.org/show_bug.cgi?id=586436 this GNOME bug.] Until this is fixed, accumulators aren't very useful. --[[User:Tweenk|Tweenk]] 01:31, 20 June 2009 (UTC)


==See Also==
==See Also==
[http://libsigc.sourceforge.net/libsigc2/docs/index.html libsigc++ reference documentation]
[http://libsigc.sourceforge.net/libsigc2/docs/index.html libsigc++ reference documentation]

Revision as of 01:31, 20 June 2009

This is a very short guide to using libsigc++ signals. They can make complex code with a lot of things to update after each change in some data structure very simple.

What are signals?

Signals are type-safe callbacks, essentially lists of function pointers on steroids. Each signal stores a list of slots. A slot defines what should be done when the signal is emitted. Typically an object emits the signal when it changes state. When a signal is emitted, the slots from the signal's slot list are executed, and sometimes a result is returned.

Note that libsigc++ signals have absolutely nothing to do with POSIX signals.

Defining signals

Signals are typically defined as public attributes like this:

class Emitter {
public:
   Emitter(std::string const &n) : _name(n), _number(0) {}
   void increaseNumber(int by) {
      _number += by;
      signal_number_changed.emit(by);
   }
   int getNumber() { return _number; }
   std::string const &getName { return _name; }
   sigc::signal<void> signal_number_changed;
private:
   int _number;
   std::string _name;
};
std::ostream &operator<<(std::ostream &o, Emitter &e) {
   o << e.getNumber();
   return o;
}

signal is a class template. The first type parameter is the return type of the slot. The remaining ones are the types of parameters passed to each slot when the signal is emitted.

Connecting to signals

The signal class has a connect method, which takes a slot and adds it to the list of the signal's slots. One type of slots are functors - objects that encapsulate a pointer to a function, or a pointer to a method. Here is a typical example:

class Receiver {
public:
   Receiver(Emitter &emitter) {
      emitter.signal_number_changed.connect(
         sigc::mem_fun(*this, &Receiver::_handleNumberChange)
      );
   }
private:
   void _handleNumberChange(int state) {
      std::cout << "The number increased by " << state << std::endl;
   }
};

sigc::mem_fun is a functor that invokes a given method on the given object when it is called. Now whenever the number stored in the Emitter class changes, all Receiver objects created with this Emitter object will have the _handleNumberChange method called.

Adaptors

As you can see, we cannot directly access the object that emitted the signal from the handling method. You might think that changing the signal definition in the Emitter class to

sigc::signal<void, Emitter &, int> signal_number_changed;

is the way to go. However, there are a few problems: first, the Emitter parameter for this slot is always the same (when you connect to another Emitter object from the same Receiver, you are creating a different slot stored in a different Emitter object). Moreover, it may be possible that some handlers only need the information about the number change - for instance, when you derive a class from Emitter and connect to the signals of the superclass, you already chave the pointer to the superclass instance in this. There is a much better way to solve this problem. Another type of slots that libsigc++ provides are called adaptors - they allow you to change the type signature of your handler function by binding constant parameters to its invocation, ignoring some parameters supplied by the signal, and more.

Binding parameters

Let's say you want to print the name of the emitter that had its number increased. Instead of modifying the signal signature (a thing you sometimes cannot do, for example when you connect to a GTK-- signal), you can bind a parameter to your functor. First you need to modify the handler function:

void _handleNumberChange(Emitter &e, int by) {
   std::cout << "Emitter " << e.getName()
             << " increased number by " << by << std::endl;
}

then bind the first parameter like this.

emitter.signal_number_changed.connect(
   sigc::bind<0>(
      sigc::mem_fun(*this, &Receiver::_handleNumberChange),
      emitter)
   )
);

sigc::bind takes an integer template parameter that tells it the zero-based index of the parameter it should bind. 0 denotes the first parameter, and the default value of -1 tells it to bind the last parameter. After this, your handler function will be called with the first parameter holding a reference to the object that emitted the signal.

Hiding parameters

Let's say you only want to print the name of the emitter and the current number, and you're not interested in how much it increased. You can then hide some of the signal's parameters.

void _handleNumberChange(Emitter &e) {
   std::cout << "Emitter << e.getName() << " changed its number." << std::endl;
}
emitter.signal_number_changed.connect(
   sigc::hide(
      sigc::bind<0>(
         sigc::mem_fun(*this, &Receiver::_handleNumberChange),
         emitter)
      )
   )
);

sigc::hide takes a template parameter just like sigc::bind. Here we use its default value. Also notice that you can easily chain adaptors. This powerful technique allows you to bind handlers to signals even if they weren't designed for each other.

Using the functors sigc::bind_return and sigc::hide_return you can also manipulate the return type. The first one changes to return type of the slot to that of a constant bound parameter. The second ignores the return value and create a slot returning void.

Note that there is an inversion of meaning: you use sigc::bind to supply something that isn't present in the signal, and sigc::hide to ignore something that isn't needed by the slot. With return adaptors, it's the other way around: you use sigc::bind_return to supply something that isn't present in the slot, and sigc::hide_return to ignore something that isn't needed by the signal.

Advanced: reordering parameters

Another powerful type of adaptor is sigc::group. It allows you to execute arbitrary mathematical expressions on the parameter list before pasing it to the handler. Here is an example.

class SomeGeometricObject {
...
   sigc::signal<
      void,
      Geom::Point const & /* old_position */,
      Geom::point const & /* new_position */ > signal_position_changed;
void _handlePositionChange(Geom::Point const &delta) {
   std::cout << "Object moved by"
             << ": X=" << delta[Geom::X]
             << ", Y=" << delta[Geom::Y] << std::endl;
}
some_object.signal_position_changed.connect(
   sigc::group(
      sigc::mem_fun(*this, &Receiver::_handlePositionChange),
      sigc::_2 - sigc::_1
   )
);

The objects sigc::_1 and sigc::_2 are called lambda expressions, and are used to denote the first and second parameter of the signal. The expression will be evaluated at slot call time, thanks to operator overloading. Note however that if you use variables other than lambdas, their values will be evaluated at connection time. (It would a very bad idea to modify the slot's expression after it was connected anyway.)

Return values of signals

Signals can return a value. By default, they return the value returned from the last evaluated slot, or the default value for this return type of there was none (normally zero for numerical types, NULL for pointers, false for booleans, etc.).

signal<double, void> signal_get_length;
double length_from_last_slot = signal_request_size.emit();

This isn't very powerful, because you lose data from other slots. libsigc++ provides a powerful mechanism for handling return values from multiple slots, called accumulators.

Accumulators

Accumulator is a small class template that defines a signal's return type and a call function that evaluates the slot list of a signal. (TODO: finish)

WARNING: I have discovered a bug in libsigc++ that makes accumulators severely broken, because their result type cannot be different from the result type of the slots. See this GNOME bug. Until this is fixed, accumulators aren't very useful. --Tweenk 01:31, 20 June 2009 (UTC)

See Also

libsigc++ reference documentation