Boost logo

Boost Users :

Subject: Re: [Boost-users] [signals2][review] The review of the signals2 library (formerly thread_safe_signals) begins today, Nov 1st
From: Nat Goodspeed (nat_at_[hidden])
Date: 2009-02-11 15:08:08


Sorry for the long delay... I got behind on the Boost list and am only
gradually catching up. :-(

>> Johan Råde wrote:

>>> Frank also mentioned the possibility of adding
>>> a thread safe version of boost::trackable
>>> based on boost::enable_shared_from_this.

Frank Mori Hess wrote:

> I have added a thread-UNsafe boost::signals2::trackable to svn to ease
> porting of single-threaded Boost.Signals code that doesn't need to be made
> thread-safe.

That will be useful transitionally if we can count on a thread-safe
boost::signals2::trackable arriving later. If signals2::trackable would
only ever be thread-unsafe, it's not useful to us.

I recently introduced into our code some machinery based on
boost::signal. Others are concurrently working on multithreading. I
don't want my new functionality to become a source of difficult race
bugs -- or even to be /perceived/ that way. I want to avoid a label of:
"this mechanism is thread-unsafe, avoid it in all new code."

Accordingly, I've just replaced boost::signal with
boost::signals2::signal, and so forth, throughout our code.

There were several existing uses of boost::trackable. I've coded around
them in several ways, as described below. Since I didn't introduce them,
I don't know the author's intent.

> Would you give a little more detail on what parts of boost::trackable
> porting you've found the most painful?

Those of you uninterested in details can skip the rest. :-)

My Holler class contains a boost::signals2::signal<void(const Data&)>.
For simple usage ignoring lifespan issues (e.g. connecting a free
function), I have a Holler::listen(const slot_type&) method. It's nice
that with boost::trackable, the same listen() method Just Works.

With an instance 'smartptr' of ListenerClass derived from
boost::trackable, I'd like to write something like:

     holler.listen(boost::bind(&ListenerClass::method, smartptr, _1));

If my listen() method were able to tease apart the object returned from
bind(), it could detect the case of a shared_ptr<boost::trackable subclass>.

Since I don't know how to do that, I've introduced a number of
alternative ways to get disconnect-on-destruction. I don't yet know
which, if any of them, will become the prevalent idiom. None is as
easy/foolproof as boost::trackable, since each requires the caller to
explicitly request connection management.

Maybe I'm overlooking something that would make my life much easier --
suggestions welcome!

1. I've introduced a template Holler::listen() overload like this:

template <class CLASS, typename POINTEE>
connection listen(void (CLASS::*method)(const Data&),
                   const boost::shared_ptr<POINTEE>& pointer);

Thus, instead of writing:

     holler.listen(boost::bind(&SomeClass::method, ptr, _1));

you'd write:

     holler.listen(&SomeClass::method, ptr);

This method instantiates a slot_type object, passes 'pointer' to its
track() method and then calls the other listen() overload:

     slot_type listener(boost::bind(method, pointer.get(), _1));
     // n.b. gcc 3.3 doesn't like listener(method, pointer.get(), _1)
     listener.track(pointer);
     return listen(listener);

2. For transient objects not managed by shared_ptr, I've also introduced
a Trackable base class containing
    boost::ptr_vector<boost::signals2::scoped_connection> mConnections;

The Trackable::track(const connection& c) method does this:
     mConnections.push_back(new scoped_connection(c));

So destroying a Trackable subclass object disconnects any connections
passed to track().

Again, if my Holler::listen() method could detect a bind() involving a
TrackableSubclass*, it could implicitly call Trackable::track().

2a. You can explicitly engage track() using syntax such as:

listenobj.track(holler.listen(boost::bind(&TrackableSubclass::method,
                                            listenobj,
                                            _1)));

Trackable also defines a suite of listenTo() methods:

2b. connection listenTo(Holler&, const slot_type&);

2c. // harmonious with the Holler::listen() overload
     template <class CLASS, typename POINTER>
     connection listenTo(Holler&,
                         void (CLASS::*method)(const Data&),
                         const POINTER& pointer);
     // doesn't need to be boost::shared_ptr because we're
     // using Trackable::track() rather than slot_type::track()

2d. // for a Trackable subclass object to bind one of its own methods
     template <class CLASS>
     connection listenTo(Holler&,
                         void (CLASS::*method)(const Data&));

3. One of my colleagues has a class that (with my changes) now also
wraps a boost::signals2::signal. He strongly dislikes the variation
between my listen(const slot_type&) and listen(method ptr, shared_ptr)
methods, so he's changed his own connectFor() method as follows:

     template <typename POINTER>
     connection connectFor(const slot_type& slot, const POINTER& ptr)
     {
         connection c(mSignal.connect(slot));
         Trackable* isTrackable(dynamic_cast<Trackable*>(ptr));
         if (isTrackable)
         {
             isTrackable->track(c);
         }
         return c;
     }

(Yes, this could be made cleverer to notice when POINTER is actually a
boost::shared_ptr<SOMETHING>. Next iteration.)

So you subscribe to his class with a uniform call such as:

     obj.connectFor(boost::bind(&SomeClass::method, aPointer, _1),
                    aPointer);

But then you have to pass a NULL pointer for the case of a free function
-- leaving open the possibility that someone will carelessly
copy-and-paste the wrong example instance and pass NULL with a
boost::bind() expression.

As I said above, every one of these approaches requires coding something
other than the intuitive listen(boost::bind(etc.)) call, meaning you
must know when you need to request connection management. The variety of
options is undoubtedly a bad thing: how to choose? You must know way
more than you should about what I had in mind.

A thread-safe boost::signals2::trackable mechanism would allow us to
drop back to a single universal call:

     holler.listen(boost::bind(&SomeClass::method, ptr, _1));

with connection management automatically engaged as appropriate.

A transitional thread-unsafe boost::signals2::trackable would be tenable
for now...

but if boost::signals2::trackable were only ever going to be
thread-unsafe, I couldn't encourage people to use it: we'd be
responsible for propagating new thread-unsafety through an application
to which we're even now adding threads.


Boost-users list run by williamkempf at hotmail.com, kalb at libertysoft.com, bjorn.karlsson at readsoft.com, gregod at cs.rpi.edu, wekempf at cox.net