views:

377

answers:

3

I've currently got a class that can notify a number of other objects via callbacks:

class Callback {
   virtual NodulesChanged() =0;
   virtual TurkiesTwisted() =0;
};

class Notifier
{
  std::vector<Callback*> m_Callbacks;

  void AddCallback(Callback* cb) {m_Callbacks.push(cb); }
  ...
  void ChangeNodules() {
     for (iterator it=m_Callbacks.begin(); it!=m_Callbacks.end(); it++) {
        (*it)->NodulesChanged();
     }
  }
};

I'm considering changing this to use boost's signals and slots as it would be beneficial to reduce the likelihood of dangling pointers when the callee gets deleted, among other things. However, as it stands boost's signals seems more oriented towards dealing with function objects. What would be the best way of adapting my code to still use the callback interface but use signals and slots to deal with the connection and notification aspects?

+2  A: 

boost::signals is pretty flexible when it comes to what you bind to a signal. You can use a function object, but you can also just use a function pointer or use boost::bind to make almost any kind of function into a function object. Here is what your example might look like, although there may be better ways.

#include <boost/signals.hpp>

class Notifier
{
public:
    boost::signal< void() > NodulesChanged;

    void ChangeNodules()
    {
        //Just call the signal and all connected slots will be called.
        NodulesChanged();
    }
};

To add a callback, you can simply

void callback1()
{
    //do callback stuff
}

void callback2()
{
    //do callback stuff
}

int main()
{
    Notifier n;
    n.NodulesChanged.connect(&callback1);
    n.NodulesChanged.connect(&callback2);

    //calls callback1 & 2.
    n.ChangeNodules();
}

If you wanted to connect a member function with arguments as a slot, you could do something like this:

class Notifier
{
public:
    boost::signal< void ( double ) > ProgressSignal;
};

class OtherClass
{
public:
    void UpdateProgress(double pct);
};

int main()
{
    Notifier n;
    OtherClass oc;

    n.ProgressSignal.connect(boost::bind(&OtherClass::UpdateProgress, &oc, _1));

    //Calls oc.UpdateProgress(0);
    n.ProgressSignal(0);
}

Warning: None of this has been compiled or tested.

Jason
Doesn't this require each method of the callback interface to have a separate signal though? So a call to AddCallback() would have to add the callback to each one of the signals. Perhaps there is no other way than to consider each method of the callback a separate signal?
the_mandrill
+2  A: 

This solution allows you to use the same signal object even if Callback's methods have different signatures.

#include <iostream>
#include <boost/signal.hpp>

//------------------------------------------------------------------------------
class Callback
{
public:
   virtual void NodulesChanged() =0;
   virtual void TurkiesTwisted(int arg) =0;
};

//------------------------------------------------------------------------------
class FooCallback : public Callback
{
public:
   void NodulesChanged() {std::cout << "Foo nodules changed\n";}
   void TurkiesTwisted(int arg) {std::cout << "Foo " << arg << " turkies twisted\n";}
};

//------------------------------------------------------------------------------
class BarCallback : public Callback
{
public:
   void NodulesChanged() {std::cout << "Bar nodules changed\n";}
   void TurkiesTwisted(int arg) {std::cout << "Bar " << arg << " turkies twisted\n";}
};

//------------------------------------------------------------------------------
class CallbackInvoker
{
public:
    virtual void operator()(Callback* callback) const {};
};

//------------------------------------------------------------------------------
class NoduleChangedInvoker : public CallbackInvoker
{
public:
    void operator()(Callback* callback) const {callback->NodulesChanged();}
};

//------------------------------------------------------------------------------
class TurkiesTwistedInvoker : public CallbackInvoker
{
public:
    TurkiesTwistedInvoker(int arg) : arg_(arg) {}
    void operator()(Callback* callback) const {callback->TurkiesTwisted(arg_);}

private:
    int arg_;
};

//------------------------------------------------------------------------------
class CallbackSlot
{
public:
    CallbackSlot(Callback* callback) : callback_(callback) {}

    void operator()(const CallbackInvoker& invoker) {invoker(callback_);}

private:
    Callback* callback_;
};

//------------------------------------------------------------------------------
class Subject
{
public:
    typedef boost::signal<void (const CallbackInvoker&)> SignalType;

    boost::signals::connection Connect(Callback* callback)
            {return signal_.connect(CallbackSlot(callback));}
    void OnNoduleChanged() {signal_(NoduleChangedInvoker());}
    void OnTurkiedTwisted(int arg) {signal_(TurkiesTwistedInvoker(arg));}

private:
    SignalType signal_;
};

//------------------------------------------------------------------------------
int main()
{
    Subject subject;
    FooCallback fooCb;
    BarCallback barCb;

    subject.Connect(&fooCb);
    subject.Connect(&barCb);

    subject.OnNoduleChanged();
    subject.OnTurkiedTwisted(42);
}

This outputs:

Foo nodules changed
Bar nodules changed
Foo 42 turkies twisted
Bar 42 turkies twisted

CallbackSlot is the function object stored in the boost::signal, and contains a pointer to a concrete Callback object. When you invoke the boost::signal, you have to pass it a CallbackInvoker concrete object which bundles any callback arguments and which knows how to invoke the appropriate Callback method.

There might be a way to avoid the CallbackInvoker boilerplate code using Boost.Lamda, but I'm not very familiar with that Boost library.

You'll probably want to use boost::shared_ptr<Callback> instead of Callback* to avoid memory leaks and dangling pointers.

Emile Cormier
I just realized that my answer is a combination of the Observer and Visitor patterns. :-)
Emile Cormier
Thanks, that's an interesting solution. The client code in `main()` is close to what I'd like to achieve, but it seems a shame to need a `CallbackInvoker` specified for each method of the interface.
the_mandrill
There might be a way to eliminate some invoker boilerplate code using templates. Working on it.... (I find your question quite interesting, BTW.)
Emile Cormier
See my other answer. I think you'll like it. :-)
Emile Cormier
+2  A: 

Compared to my other answer, this solution is much more generic and eliminates boilerplate code:

#include <iostream>
#include <boost/bind.hpp>
#include <boost/signal.hpp>

///////////////////////////////////////////////////////////////////////////////
// GENERIC REUSABLE PART FOR ALL SUBJECTS
///////////////////////////////////////////////////////////////////////////////

//-----------------------------------------------------------------------------
template <class CallbackType>
class CallbackInvoker
{
public:
    virtual ~CallbackInvoker() {}
    virtual void operator()(CallbackType* callback) const {};
};

//-----------------------------------------------------------------------------
template <class CallbackType, class Binding>
class BoundInvoker : public CallbackInvoker<CallbackType>
{
public:
    BoundInvoker(const Binding& binding) : binding_(binding) {}
    void operator()(CallbackType* callback) const {binding_(callback);}

private:
    Binding binding_;
};

//-----------------------------------------------------------------------------
template <class CallbackType>
class CallbackSlot
{
public:
    CallbackSlot(CallbackType* callback) : callback_(callback) {}
    void operator()(const CallbackInvoker<CallbackType>& invoker)
        {invoker(callback_);}

private:
    CallbackType* callback_;
};

//-----------------------------------------------------------------------------
template <class CallbackType>
class Subject
{
public:
    virtual ~Subject() {}
    boost::signals::connection Connect(CallbackType* callback)
        {return signal_.connect(CallbackSlot<CallbackType>(callback));}

protected:
    template <class Binding> void Signal(const Binding& binding)
    {
        signal_(BoundInvoker<CallbackType,Binding>(binding));
    }

private:
    boost::signal<void (const CallbackInvoker<CallbackType>&)> signal_;
};


///////////////////////////////////////////////////////////////////////////////
// THIS PART SPECIFIC TO ONE SUBJECT
///////////////////////////////////////////////////////////////////////////////
//------------------------------------------------------------------------------
class MyCallback
{
public:
    virtual ~MyCallback() {}
    virtual void NodulesChanged() =0;
    virtual void TurkiesTwisted(int arg) =0;
};

//-----------------------------------------------------------------------------
class FooCallback : public MyCallback
{
public:
    virtual ~FooCallback() {}
    void NodulesChanged() {std::cout << "Foo nodules changed\n";}
    void TurkiesTwisted(int arg)
        {std::cout << "Foo " << arg << " turkies twisted\n";}
};

//-----------------------------------------------------------------------------
class BarCallback : public MyCallback
{
public:
    virtual ~BarCallback() {}
    void NodulesChanged() {std::cout << "Bar nodules changed\n";}
    void TurkiesTwisted(int arg)
        {std::cout << "Bar " << arg << " turkies twisted\n";}
};

//-----------------------------------------------------------------------------
class MySubject : public Subject<MyCallback>
{
public:
    void OnNoduleChanged()
        {this->Signal(boost::bind(&MyCallback::NodulesChanged, _1));}
    void OnTurkiedTwisted(int arg)
        {this->Signal(boost::bind(&MyCallback::TurkiesTwisted, _1, arg));}
};

///////////////////////////////////////////////////////////////////////////////
// CLIENT CODE
///////////////////////////////////////////////////////////////////////////////

//-----------------------------------------------------------------------------
int main()
{
    MySubject subject;
    FooCallback fooCb;
    BarCallback barCb;

    subject.Connect(&fooCb);
    subject.Connect(&barCb);

    subject.OnNoduleChanged();
    subject.OnTurkiedTwisted(42);
}

Hooray for boost::bind! :-)

Emile Cormier
Very elegant. Mind if I ask you if you are searching for a boost-related open source code?
the_drow
Very neat -- the reusable code is a great bonus. `boost::bind` is really the biz... Great answer.
the_mandrill
@the_drow: I'm sorry, I don't quite understand your question. Did you want to know if I'd like to contribute to a Boost-related open-source project?
Emile Cormier