Search code examples
c++exceptionboostsignalsboost-signals2

Any way to cancel signal propagation in boost signals2 without exceptions?


I'd like to use boost::signals2 to handle event notification in my C++ app. I'm hoping to implement something with similar functionality to browser DOM events, specifically the ability to stop the propagation of an event so that the current receiver is the last one to know about a signal and subsequent receivers are not called. (see http://www.w3.org/TR/DOM-Level-3-Events/#events-event-type-stopImmediatePropagation for more on how this works in browsers)

I have a hypothetical App class with a signal called thingHappened. It's likely there'll only be one App instance, with several other Widget classes of varying types that will connect to thingHappened to receive ThingEvent notifications. Sometimes the widget would like to consume (stop) the ThingEvent so that no other Widgets are notified.

At first I wondered if I could achieve this with a shared_connection_block but now I understand that this only suppresses one connection at a time. Initially I passed a shared_ptr<ThingEvent> to my signal but once the signal was called there was no way to intervene with its propagation. If I pass a shared_ptr I could have signal receivers check a value on the event and return if it's set, but I don't want to push that detail off to users of my library.

The solution I've found is to pass a ThingEvent on the stack so that it is copied for each receiver. If I set mStopPropagation on the event then when it is destroyed I can throw an exception and the signal calls terminate. The downside to this is that I need my own try/catch at the point where the signal is called, and stylistically it means I'm using an exception for an unexceptional purpose. Is there a better way?

Here's my hypothetical App class, with a signal thingHappened:

class App
{
public:
    boost::signals2::signal<void (class ThingEvent)> thingHappened;
};

My ThingEvent class, with some data about the event (e.g. type) and an mStopPropagation property that will cause an exception to be thrown if it is set in the destructor:

class ThingEvent
{
public:
    ThingEvent(string type): mType(type), mStopPropagation(false) { }

    ~ThingEvent() 
    {
        if (mStopPropagation) {
            throw exception();
        }
    }

    void stopPropagation() { mStopPropagation = true; }

    string getType() { return mType; }

private:
    string mType;
    bool mStopPropagation;

};

Here's a sample signal consumer, a Widget, that will call stopPropagation() on an event if the type is "goat":

class Widget
{
public:
    Widget(string name): mName(name) {}

    ~Widget() {}

    void thingHappened(ThingEvent thing)
    { 
        cout << thing.getType() << " thingHappened in widget " << mName << endl; 

        if (thing.getType() == "goat") {
            thing.stopPropagation();
        }
    }

    string getName() 
    {
        return mName;
    }

private:
    string mName;
};

Finally, here's a quick main() function that uses these classes:

int main()
{
    App app;

    Widget w1("1");
    Widget w2("2");
    Widget w3("3");

    boost::signals2::connection c1 = app.thingHappened.connect(boost::bind(&Widget::thingHappened, &w1, _1));
    boost::signals2::connection c2 = app.thingHappened.connect(boost::bind(&Widget::thingHappened, &w2, _1));
    boost::signals2::connection c3 = app.thingHappened.connect(boost::bind(&Widget::thingHappened, &w3, _1));

    // all three widgets will receive this
    app.thingHappened(ThingEvent("otter"));

    {
        // suppress calls to c2
        boost::signals2::shared_connection_block block(c2,true);
        // only w1 and w3 will receive this
        app.thingHappened(ThingEvent("badger"));
    }

    // Widgets call ThingEvent::stopPropagation() if the type is "goat"
    try {
        // only w1 will receive this
        app.thingHappened(ThingEvent("goat"));
    } catch (exception &e) {
        // ThingEvent's destructor throws if mStopPropagation is true
        std::cout << "exception thrown by thingHappened(goat)" << std::endl;
    }

    return 0;
}

If you have boost to hand (I'm using 1.44) and you want to compile it, the full code and Makefile are at https://gist.github.com/1445230


Solution

  • You can do this with a custom signal combiner. These are explained in the "Signal Return Values (Advanced)" section of the boost.signals tutorial: http://www.boost.org/doc/libs/1_48_0/doc/html/signals/tutorial.html#id3070223

    Here's the gist of it:

    struct MyCombiner {
        typedef bool result_type;
        template <typename InputIterator> result_type operator()(InputIterator aFirstObserver, InputIterator aLastObserver) const {
            result_type val = false;
            for (; aFirstObserver != aLastObserver && !val; ++aFirstObserver)  {
                val = *aFirstObserver;            
            }
            return val;
        }
    };
    

    The input iterators to operator() refer to the collection of slots for the signal. Each time you dereference one of the iterators, it calls the slot. So you can let one of the slots return a value to indicate that it doesn't want any further slots to be called.

    Then you just pass it in to the second template arg of your signal:

    boost::signals2::signal<bool(ThingEvent), MyCombiner> sig;
    

    Now you can implement thingHappened to return a bool value indicating whether or not you want the signal to be stopped.