Tuesday, January 4, 2011

Quest for sane signals in Qt - step 2 (reconstructing parameters)

The second part of this problem involves turning our void** parameter array into the correct parameters and invoking our function.

At this point we'll assume that the type information exists in the form of a function signature, which, being a type, can be analyzed through metaprogramming; a library exists to do this within boost called FunctionTypes. This assumption is safe to make since this information is going to either be provided by the user or through the function data available from the signature; this mechanism will be discussed later. For now, we'll start with a function signature and devise a way to call some function F compatible with that signature using a mechanism similar to what qt_metacall is going to be forwarding. Here is the driver code for that problem:

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

struct converter
{
virtual ~converter() {}

virtual void call(void ** args) const = 0;

};

void call_converter(converter const& cv)
{
int a0 = 42;
double a1 = 66.6;
std::string a3 = "Hello World!";

void* params[] = { &a0, &a1, &a3 };
cv.call(params);
}

void fun(std::string const& str)
{
std::cout << str << std::endl;
}

template < typename Signature >
struct sig_converter
{
...described below...
};

int main()
{
sig_converter<void(int,double,std::string)> cvt(boost::bind(&fun, _3));
call_converter(cvt);
}

None of that code is part of the solution, it is simply code meant to drive the experiment used to solve one part of that solution. The "converter" is the placeholder for the QObject and "call" is a placeholder for qt_metacall (though in the eventual solution it won't be virtual, but will instead call a virtual invoker). In our main function we want to make sure we can attach to our "signal", observing or ignoring whatever parameters are necessary to make the call.

The thing we need to do now, is fill the body of sig_converter. A good initial test will simply forward to a "function" (of the std or boost kind). We can later simply use this solution (since a signal IS a function) or devise a more generic one that has as little indirection overhead as possible. So, at this stage our object looks like so:

#include <boost/function.hpp>

template < typename Signature >
struct sig_converter
{
sig_converter(boost::function<Signature> f) : fwd(f) {}

void call(void ** params)
{
...
}

private:
boost::function<Signature> fwd;
};

That being all the straight-forward stuff that pretty much everyone is familiar with, it's time to start with the metaprogramming needed to turn 'params' into the right types, in the right order, and use them to invoke fwd(). The first thing that needs to happen is to turn the 'Signature' parameter into an mpl sequence that we can iterate:

typedef typename boost::function_types::parameter_types<Signature>::type params_t;

Next piece needed is an invoker that uses this sequence of types to call 'fwd'. As with usual with metaprogramming, the iteration to make this happen will be recursion:

template < Signature >
struct sig_converter
{
...
typedef typename boost::function_types::parameter_types<Signature>::type params_t;

template < typename FromIter = typename boost::begin<params_t>::type
, typename ToIter = typename boost::end <params_t>::type >
struct invoker
{
template < typename Args >
static void apply( boost::function<Signature> const& f
, void ** params
, Args const& args )
{
// the type of the current parameter.
typedef typename boost::mpl::deref<FromIter>::type arg_type;
// an iterator to the next parameter.
typedef typename boost::mpl::next<FromIter> ::type next_iter;

// get the value...
arg_type val = *reinterpret_cast<arg_type*>(*params++);

// Call the next iteration, having pushed our current parameter onto the param "stack"
invoker<next_iter, ToIter>::apply(f,params,boost::fusion::push_back(args,val));
}
};
};

// the end case, when we've reached the end of our iteration...
template < typename Signature >
template < typename IterTo >
struct sig_converter::invoker<IterTo,IterTo> // specialize for iterators being the same...
{
template < typename Args >
static void apply( boost::function<Signature> const& f
, void ** // unused - our arguments have been converted
, Args const& args)
{
// black magic created by boost to invoke a function with a 'fusion' sequence
boost::fusion::invoke(f,args);
}
};

And THAT is the worse of the black magic (minus what's UNDER these boost types). The way this works is that each version of sig_converter will instantiate it's internal 'invoker' with iterators to the sequence created from its Signature parameter. It, in turn will instantiate 'invoker' with the next iterator, and the next, and the next, until the end is reached. This last instantiation invokes the function. Along the way we're given type information for the current parameter, we convert it, and we iterate to the next parameter to pass on to the next instantiation.

So, that all being done, the last thing we need to do is finish up by filling in 'call' with one, simple line of code:

invoker<>::template apply<boost::fusion::nil>(fwd,params,boost::fusion::nil());

What we've done here is instantiate the beginning of the iteration by using the default arguments, and passing in an empty stack for the arguments to be pushed onto.

A lot of the ideas in this code come from the documentation for boost::function_types, namely the interpreter example. If you want to learn more, there's the boost documentation and the book, "C++ Template Metaprogramming" by Abrahams and and Gurtovoy. This book is an essential part for today's C++ developer's bookshelf.

1 comment:

  1. Very nice, I'm working on a similar problem right now, myself, and definitely agree that C++ Template Metaprogramming is essential. :D

    I found that another nice source of information is Abrahams' Boost.Python library - it takes a set of argument names and default values and uses it to do some more black magic (registering them with Python function objects.)

    ReplyDelete