Difference between revisions of "Using libsigc++ signals"
Line 43: | Line 43: | ||
} | } | ||
private: | private: | ||
void _handleNumberChange(int | void _handleNumberChange(int increment) { | ||
std::cout << "The number increased by " << | std::cout << "The number increased by " << increment << std::endl; | ||
} | } | ||
}; | }; |
Revision as of 20:51, 24 September 2013
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, int> signal_number_changed; private: int _number; std::string _name; }; std::ostream &operator<<(std::ostream &o, Emitter &e) { o << e.getNumber(); return o; }
Several Inkscape classes use private signal attributes and define accessor methods for connecting slots, but this prevents you from using some advanced features. Wrapping them all in accessors isn't worth the hassle.
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 increment) { std::cout << "The number increased by " << increment << 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. To use references to variables instead of their current values, use sigc::ref.
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> signal_get_length; double length_from_last_slot = signal_get_length.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. Here is an example accumulator that calculates the sum of the values returned by slots.
template<class T> struct sum_accumulator { typedef T result_type; template<class I> result_type operator()(I first, I last) { T sum = 0; for (; first != last; first ++) sum += *first; return sum; }
};
There a few important things to notice:
- Each accumulator must have two public members: a type called result_type which defines the return value of the signal's emit() function, and an overloaded operator(), taking two arguments of some type (the type in question is called sigc::internal::slot_iterator_buf, but it's better to just use a template).
- The parameters of the function call operator are two iterators, pointing to the start and the end of the slot list. The expression *i evaluates the slot (i.e. calls it and returns the result).
- It is safe to write things like sum += (*i) * (*i); because the results are buffered - each slot is evaluated at most once in each signal emission.
To use the accumulator, you need to changed the signal definition a bit.
sigc::signal<double>::accumulated<sum_accumulator<int> > signal_get_length;
double sum_of_lengths = signal_get_length.emit();
Now sum_of_lengths contains the sum of return values from all the slots.
WARNING: There is an old bug in libsigc++ that makes accumulators severely broken, because their result type must be implicitly convertible to and from the slot return type. See this GNOME bug. It will be fixed in the next libsigc++ release. --Tweenk 09:27, 31 December 2009 (UTC)
Conventions and tips
- Your signals should only have parameters that can change with each invocation. For example, a position change signal could have the old and new positions as the parameters. In particular, signals should never have the emitting object as a parameter. The proper way to connect to a signal from a different object is to use sigc::bind like this:
class SomeObject { public: sigc::signal<void> signal_update; ... }; class OtherObject { ... void onUpdate(SomeObject &so); }; ... some_object.signal_update.connect( sigc::bind<0>( sigc::mem_fun(other_object, &OtherObject::onUpdate), some_object));
In the example, a reference is bound (because the SomeObject parameter will never need to be NULL), but you can also use a pointer.
- Use the C++ wrappers for GTK+ as a reference for good signal parameter choices.
- The memory and performance overhead of adding signals to an object is quite small - most things are handled at compile time thanks to the use of templates.
- When using Boost shared pointers, you cannot bind them as the "emitting object" parameter to slots. That's because the shared pointer will be copied into the object's slot list, keeping it in memory indefinitely. You need to use a weak pointer, which won't prevent the object's desctruction, but can be used to obtain a regular shared pointer. See Shared pointers for a more detailed guide.
See Also
libsigc++ reference documentation Gnome bug #586436 - epic accumulator fail