This tutorial is not meant to be read linearly. Its top-level structure roughly separates different concepts in the library (e.g., handling calling multiple slots, passing values to and from slots) and in each of these concepts the basic ideas are presented first and then more complex uses of the library are described later. Each of the sections is marked Beginner, Intermediate, or Advanced to help guide the reader. The Beginner sections include information that all library users should know; one can make good use of the Signals library after having read only the Beginner sections. The Intermediate sections build on the Beginner sections with slightly more complex uses of the library. Finally, the Advanced sections detail very advanced uses of the Signals library, that often require a solid working knowledge of the Beginner and Intermediate topics; most users will not need to read the Advanced sections.
Boost.Signals has two syntactical forms: the preferred form and the compatibility form. The preferred form fits more closely with the C++ language and reduces the number of separate template parameters that need to be considered, often improving readability; however, the preferred form is not supported on all platforms due to compiler bugs. The compatible form will work on all compilers supported by Boost.Signals. Consult the table below to determine which syntactic form to use for your compiler. Users of Boost.Function, please note that the preferred syntactic form in Signals is equivalent to that of Function's preferred syntactic form.
Preferred Syntax | Compatible Syntax |
---|---|
|
|
If your compiler does not appear in this list, please try the preferred syntax and report your results to the Boost list so that we can keep this table up-to-date.
The following example writes "Hello, World!" using signals and
slots. First, we create a signal sig
, a signal that takes
no arguments and has a void return value. Next, we connect the
hello
function object to the signal using the
connect
method. Finally, use the signal sig
like a function to call the slots, which in turns invokes
HelloWorld::operator()
to print "Hello, World!".
Preferred Syntax | Compatible Syntax |
---|---|
struct HelloWorld { void operator()() const { std::cout << "Hello, World!" << std::endl; } }; // ... // Signal with no arguments and a void return value boost::signal<void ()> sig; // Connect a HelloWorld slot HelloWorld hello; sig.connect(hello); // Call all of the slots sig(); |
struct HelloWorld { void operator()() const { std::cout << "Hello, World!" << std::endl; } }; // ... // Signal with no arguments and a void return value boost::signal0<void> sig; // Connect a HelloWorld slot HelloWorld hello; sig.connect(hello); // Call all of the slots sig(); |
Calling a single slot from a signal isn't very interesting, so we can make the Hello, World program more interesting by splitting the work of printing "Hello, World!" into two completely separate slots. The first slot will print "Hello" and may look like this:
struct Hello { void operator()() const { std::cout << "Hello"; } };
The second slot will print ", World!" and a newline, to complete the program. The second slot may look like this:
struct World { void operator()() const { std::cout << ", World!" << std::endl; } };
Like in our previous example, we can create a signal
sig
that takes no arguments and has a void
return value. This time, we connect both a hello
and a
world
slot to the same signal, and when we call the
signal both slots will be called.
Preferred Syntax | Compatible Syntax |
---|---|
boost::signal<void ()> sig; sig.connect(Hello()); sig.connect(World()); sig(); |
boost::signal0<void> sig; sig.connect(Hello()); sig.connect(World()); sig(); |
Now, if you compile and run this program, you might see something strange. It is possible that the output will look like this:
, World! Hello
The underlying reason is that the ordering of signals isn't
guaranteed. The signal is free to call either the Hello
slot or the World
slot first, but every slot will be
called unless something bad (e.g., an exception) occurs. Read on to
learn how to control the ordering so that "Hello, World!" always
prints as expected.
Slots are free to have side effects, and that can mean that some
slots will have to be called before others. The Boost.Signals library
allows slots to be placed into groups that are ordered in some
way. For our Hello, World program, we want "Hello" to be printed
before ", World!", so we put "Hello" into a group that must be
executed before the group that ", World!" is in. To do this, we can
supply an extra parameter at the beginning of the connect
call that specifies the group. Group values are, by default,
int
s, and are ordered by the integer <
relation. Here's how we construct Hello, World:
Preferred Syntax | Compatible Syntax |
---|---|
boost::signal<void ()> sig; sig.connect(0, Hello()); sig.connect(1, World()); sig(); |
boost::signal0<void> sig; sig.connect(0, Hello()); sig.connect(1, World()); sig(); |
This program will correctly print "Hello, World!", because the
Hello
object is in group 0, which precedes group 1 where
the World
object resides.
The group parameter is, in fact, optional. We omitted it in the first Hello, World example because it was unnecessary when all of the slots are independent. So what happens if we mix calls to connect that use the group parameter and those that don't? The "unnamed" slots (i.e., those that have been connected without specifying a group name) go into a separate group that is special in that it follows all other groups. So if we add a new slot to our example like this:
struct GoodMorning { void operator()() const { std::cout << "... and good morning!" << std::endl; } }; sig.connect(GoodMorning());
... we will get the result we wanted:
Hello, World! ... and good morning!
The last interesting point with groups of slots is the behavior
when multiple slots are connected in the same group. Within groups,
calls to slots are unordered: if we connect slots A
and
B
to the same signal with the same group name, either
A
or B
will be called first (but both will
be called). This is the same behavior we saw before with the second
version of Hello, World, where the slots could be called in the wrong
order, mangling the output.
Signals can propagate arguments to each of the slots they call. For instance, a signal that propagates mouse motion events might want to pass along the new mouse coordinates and whether the mouse buttons are pressed.
As an example, we'll create a signal that passes two
float
arguments to its slots. Then we'll create a few
slots that print the results of various arithmetic operations on these
values.
void print_sum(float x, float y) { std::cout << "The sum is " << x+y << std::endl; } void print_product(float x, float y) { std::cout << "The product is " << x*y << std::endl; } void print_difference(float x, float y) { std::cout << "The difference is " << x-y << std::endl; } void print_quotient(float x, float y) { std::cout << "The quotient is " << x/y << std::endl; } boost::signal<void, float, float> sig; sig.connect(&print_sum); sig.connect(&print_product); sig.connect(&print_difference); sig.connect(&print_quotient); sig(5, 3);
This program will print out something like the following, although the ordering of the lines may differ:
The sum is 8 The difference is 2 The product is 15 The quotient is 1.66667
So any values that are given to sig
when it is called
like a function are passed to each of the slots. We have to declare
the types of these values up front when we create the signal. The type
boost::signal<void, float, float>
means that the
signal has a void
return value and takes two
float
values. Any slot connected to sig
must
therefore be able to take two float
values.
Just as slots can receive arguments, they can also return values. These values can then be returned back to the caller of the signal through a combiner. The combiner is a mechanism that can take the results of calling slots (there many be no results or a hundred; we don't know until the program runs) and coalesces them into a single result to be returned to the caller. The single result is often a simple function of the results of the slot calls: the result of the last slot call, the maximum value returned by any slot, or a container of all of the results are some possibilities.
We can modify our previous arithmetic operations example slightly so that the slots all return the results of computing the product, quotient, sum, or difference. Then the signal itself can return a value based on these results to be printed:
Preferred Syntax | Compatible Syntax |
---|---|
float compute_product(float x, float y) { return x*y; } float compute_quotient(float x, float y) { return x/y; } float compute_sum(float x, float y) { return x+y; } float compute_difference(float x, float y) { return x-y; } boost::signal<float (float x, float y)> sig; sig.connect(&compute_product); sig.connect(&compute_quotient); sig.connect(&compute_sum); sig.connect(&compute_difference); std::cout << sig(5, 3) << std::endl; |
float compute_product(float x, float y) { return x*y; } float compute_quotient(float x, float y) { return x/y; } float compute_sum(float x, float y) { return x+y; } float compute_difference(float x, float y) { return x-y; } boost::signal2<float, float, float> sig; sig.connect(&compute_product); sig.connect(&compute_quotient); sig.connect(&compute_sum); sig.connect(&compute_difference); std::cout << sig(5, 3) << std::endl; |
This example program will output either 8
,
1.6667
, 15
, or 2
, depending on
the order that the signals are called. This is because the default
behavior of a signal that has a return type (float
, the
first template argument given to the boost::signal
class
template) is to call all slots and then return the result returned by
the last slot called. This behavior is admittedly silly for this
example, because slots have no side effects and the result is
essentially randomly chosen from the slots.
A more interesting signal result would be the maximum of the values returned by any slot. To do this, we create a custom combiner that looks like this:
template<typename T> struct maximum { typedef T result_type; template<typename InputIterator> T operator()(InputIterator first, InputIterator last) const { // If there are no slots to call, just return the // default-constructed value if (first == last) return T(); T max_value = *first++; while (first != last) { if (max_value < *first) max_value = *first; ++first; } return max_value; } };
The maximum
class template acts as a function
object. Its result type is given by its template parameter, and this
is the type it expects to be computing the maximum based on (e.g.,
maximum<float>
would find the maximum
float
in a sequence of float
s). When a
maximum
object is invoked, it is given an input iterator
sequence [first, last)
that includes the results of
calling all of the slots. maximum
uses this input
iterator sequence to calculate the maximum element, and returns that
maximum value.
We actually use this new function object type by installing it as a combiner for our signal. It is supplied via a named template parameter like this:
Preferred Syntax | Compatible Syntax |
---|---|
boost::signal<float (float x, float y), maximum<float> > sig; |
boost::signal2<float, float, float, maximum<float> > sig; |
Now we can connect slots that perform arithmetic functions and use the signal:
sig.connect(&compute_quotient); sig.connect(&compute_product); sig.connect(&compute_sum); sig.connect(&compute_difference); std::cout << sig(5, 3) << std::endl;
The output of this program will be 15
, because regardless
of the order in which the slots are called, the product of 5 and 3
will be larger than the quotient, sum, or difference.
In other cases we might want to return all of the values computed by the slots together, in one large data structure. This is easily done with a different combiner:
template<typename Container> struct aggregate_values { typedef Container result_type; template<typename InputIterator> Container operator()(InputIterator first, InputIterator last) const { return Container(first, last); } };Again, we can create a signal with this new combiner:
Preferred Syntax | Compatible Syntax |
---|---|
boost::signal<float (float, float), aggregate_values<std::vector<float> > > sig; sig.connect(&compute_quotient); sig.connect(&compute_product); sig.connect(&compute_sum); sig.connect(&compute_difference); std::vector<float> results = sig(5, 3); std::copy(results.begin(), results.end(), std::ostream_iterator<float>(cout, " ")); |
boost::signal2<float, float, float, aggregate_values<std::vector<float> > > sig; sig.connect(&compute_quotient); sig.connect(&compute_product); sig.connect(&compute_sum); sig.connect(&compute_difference); std::vector<float> results = sig(5, 3); std::copy(results.begin(), results.end(), std::ostream_iterator<float>(cout, " ")); |
The output of this program will contain 15, 8, 1.6667, and 2 (but not
necessarily in that order). It is interesting here that the first
template argument for the signal
class,
float
, is not actually the return type of the
signal. Instead, it is the return type used by the connected slots and
will also be the value_type
of the input iterators passed
to the combiner. The combiner itself is a function object and its
result_type
member type becomes the return type of the
signal.
Slots aren't expected to exist indefinately after they are connected. Often slots are only used to receive a few events and are then disconnected, and the programmer needs control to decide when a slot should no longer be connected.
The entry point for managing connections explicitly is the
boost::signals::connection
class. The
connection
class uniquely represents the connection
between a particular signal and a particular slot. The
connected()
method checks if the signal and slot are
still connected, and the disconnect()
method disconnects
the signal and slot if they are connected before it is called. Each
call to the signal's connect()
method returns a
connection object, which can be used to determine if the connection
still exists or to disconnect the signal and slot.
boost::signals::connection c = sig.connect(HelloWorld()); if (c.connected()) { // c is still connected to the signal sig(); // Prints "Hello, World!" } c.disconnect(); // Disconnect the HelloWorld object assert(!c.connected()); c isn't connected any more sig(); // Does nothing: there are no connected slots
The boost::signals::scoped_connection
class
references a signal/slot connection that will be disconnected when the
scoped_connection
class goes out of scope. This ability
is useful when a connection need only be temporary, e.g.,
{ boost::signals::scoped_connection c = sig.connect(ShortLived()); sig(); // will call ShortLived function object } sig(); // ShortLived function object no longer connected to sig
Preferred Syntax | Compatible Syntax |
---|---|
class NewsItem { /* ... */ }; boost::signal<void (const NewsItem& latestNews)> deliverNews; |
class NewsItem { /* ... */ }; boost::signal1<void, const NewsItem&> deliverNews; |
Clients that wish to receive news updates need only connect a
function object that can receive news items to the
deliverNews
signal. For instance, we may have a special
message area in our application specifically for news, e.g.,:
struct NewsMessageArea : public MessageArea { public: // ... void displayNews(const NewsItem& news) const { messageText = news.text(); update(); } }; // ... NewsMessageArea newsMessageArea = new NewsMessageArea(/* ... */); // ... deliverNews.connect(boost::bind(&NewsMessageArea::displayNews, newsMessageArea, _1));
However, what if the user closes the news message area, destroying
the newsMessageArea
object that deliverNews
knows about? Most likely, a segmentation fault will occur. However,
with Boost.Signals one need only make NewsMessageArea
trackable, and the slot involving
newsMessageArea
will be disconnected when
newsMessageArea
is destroyed. The
NewsMessageArea
class is made trackable by deriving
publicly from the boost::signals::trackable
class, e.g.:
struct NewsMessageArea : public MessageArea, public boost::signals::trackable { // ... };
At this time there is a significant limitation to the use of
trackable
objects in making slot connections: function
objects built using Boost.Bind are understood, so that any place a
trackable
object appears in a bind expression. However,
user-defined function objects and function objects from other
libraries (e.g., Boost.Function or Boost.Lambda) do not implement the
required interfaces for trackable
object detection, and
will silently ignore any bound trackable objects. Future
versions of the Boost libraries will address this limitation.
Slots in the Boost.Signals library are created from arbitrary
function objects, and therefore have no fixed type. However, it is
commonplace to require that slots be passed through interfaces that
cannot be templates. Slots can be passed via the
slot_type
for each particular signal type and any valid
function object can be passed to a slot_type
parameter. For instance:
Preferred Syntax | Compatible Syntax |
---|---|
class Button { typedef boost::signal<void (int x, int y)> OnClick; public: void doOnClick(const OnClick::slot_type& slot); private: OnClick onClick; }; void Button::doOnClick(const OnClick::slot_type& slot) { onClick.connect(slot); } |
class Button { typedef boost::signal2<void, int x, int y> OnClick; public: void doOnClick(const OnClick::slot_type& slot); private: OnClick onClick; }; void Button::doOnClick(const OnClick::slot_type& slot) { onClick.connect(slot); } |
The doOnClick
method is now functionally equivalent
to the connect
method of the onClick
signal,
but the details of the doOnClick
method can be hidden in
an implementation detail file.