Saturday, 17 October 2009

Using Boost Function with Qt, Part 2.

[Following up this post]

Johan asks:

please, please post that follow-up. I'm new to Qt and have just run into this problem. While searching the net, I think your solution seems like the best approach!

Who am I to deny a seeker of knowledge? Having used this method in a couple of projects now, I think it is fairly sound and easy to maintain. So here goes.
The main problem with the solution outlined in the earlier post was that it could not handle multiple arguments. On that same post, Ken posted his solution apparently inspired by mine. His solution also handles multiple arguments but does it by implementing qt_metacall, i.e., mimicking moc. This is probably the most scalable solution and I'll probably give it a second look when I need to do this again. The only problem might be requiring exact matches for types but I am not sure how much of an issue this is for code I have written.

To review, the problem is that we want to use function objects (Boost Bind, Boost Function, etc) as Qt slots because sometimes it is too much work to create stateful slots.

My solution looked something like this:

struct SignalHandler0 : QObject
{
private:
Q_OBJECT
public:
SignalHandler0(QObject * parent,
boost::function const & f):
QObject(parent), // parent will delete this object when destructed
m_f(f) {}

public slots:
void
handleSignal()
{
try
{
m_f();
}
catch(...)
{
// Cannot throw exceptions from signals.
ASSERT_BUG_HERE
}
}
private:
boost::function m_f;
};

bool
connect(QObject * sender, const char * signal,
boost::function const & f)
{
return QObject::connect(sender,signal,
// Note: Not a leak as sender will delete the handler when destructed
new SignalHandler0(sender,f),SLOT(handleSignal()));
}
To extend it to a single argument, we might do:
struct SignalHandler1 : QObject
{
private:
Q_OBJECT
public:
SignalHandler1(QObject * parent,
boost::function const & f):
QObject(parent), // parent will delete this object when destructed
m_f(f) {}

public slots:
void
handleSignal(QString const & a)
{
try
{
m_f(a);
}
catch(...)
{
// Cannot throw exceptions from signals.
ASSERT_BUG_HERE
}
}
private:
boost::function m_f;
};

That is, create a new class with the right signature. So you might think: well that screams for class templates! Unfortunately, Qt does not support class templates. Doh.

So basically, we write it out:

struct SignalHandler : public QObject
{
template
SignalHandler(QObject * parent,boost::function f):
QObject(parent),
m_handler(new SignalHandlerImpl(f))
{}

struct SignalHandlerBase
{
virtual ~SignalHandlerBase();
}

template
struct SignalHandlerImpl
{
SignalHandlerImpl(boost::function f):m_f(f){}
boost::function f;
}
public slots:
void handleSignal(void);
void handleSignal(QString const &);
...

private:
shared_ptr m_handler;
};

void
SignalHandler::handleSignal(void)
{
typename SignalHandlerImpl type;
type* handler = dynamic_cast;(m_handler.get());
ASSERT(handler);
handler->f();
}

void
SignalHandler::handleSignal(QString const & a)
{
typename SignalHandlerImpl type;
type* handler = dynamic_cast(m_handler.get());
ASSERT(handler);
handler->f(a);
}

You can use macros to generate these handlers. You will also need overloaded connect() functions:

template
QObject*
connect(QObject * sender,
const char * signal,
boost::function slot);

...
template<>
QObject*
connect(QObject * sender,
const char * signal,
boost::function slot);

template<>
QObject*
connect(QObject * sender,
const char * signal,
boost::function slot);
Again, you can use macros.

I prefer Ken's solution from a technical standpoint. It is a good evolution. The main advantage of his solution is that the amount of code you need to add scales linearly with the number of arguments you need to handle whereas with my solution, they scale with the number of signals and that there are no macros.

The main advantage of my solution is that it is simpler to understand and maintain, which might be worth the extra code.

Hope that helps!