Thursday, January 6, 2011

Quest for sane signals in Qt - step 3 (reaching the goal)

The goal for this article series was to create a method to connect any function object to a Qt signal using syntax similar to:

connect_static(object, signal, function);

Well, it turns out that this exact syntax is not going to work but the result of this article is how we can get our type checking back and connect to a Qt signal using this syntax:

connect_static(SIGINFO(object, signame, (param1,param2)), function)

Remember that 'signal' above was "signame(param1,param2)" so the main difference here is that there's an extra comma and a macro similar to "SIGNAL" in Qt. I'll explain why this is necessary.

Further, the only way I was able to come up with this syntax was to use the 'decltype' keyword that only exists in the new C++ standard. If I find a way to get rid of this requirement I'll post but for now, most of the compilers used today have versions that support this keyword and have for quite a while now. If you don't have one of these you can still use this technique, it just won't look quite as nice.

The first thing that is needed here is a component that can hold enough information for us to connect to the signal and to verify the signatures match. Because Qt casts to void* and we then cast back, the signature of the converter has to exactly match the signal.

template < typename Class, typename MemPtr >
struct siginfo
{
Class * object;
char const* signame;

typedef typename boost::function_types::parameter_types<MemPtr>::type memptr_params;
typedef typename boost::mpl::pop_front<memptr_params>::type sig_params;
typedef typename boost::mpl::push_front<sig_params,void>::type sig_types;

typedef typename boost::function_types::function_type<sig_types>::type signature;
};

All this object does is hold a pointer to the object we want to connect to, the signal name (to be fetched with Qt's SIGNAL() macro-see below) and derives the signal signature from the signature of the member pointer. We have to remove the first parameter of that signature because the member pointer "function" has the extra 'this' as its first parameter (this is how function_types defines behavior wrt member functions).

Next we need a utility function to create this object:

template < typename Class, typename MemPtr >
siginfo<Class, MemPtr> make_siginfo(Class * object, MemPtr , char const* signame)
{
siginfo<Class,MemPtr> retval = {object, signame};
return retval;
}

Nothing particularly interesting there. This function will be used by our macros because attempting to instantiate the class directly wouldn't be as easy, nor possibly even possible, otherwise.

The next thing we need is a way to get the member function from the supplied arguments. Because Qt's signals are functions (I gather there are cases when they are not, such cases won't work here) created by the MOC, we can grab the address and treat them just like normal member functions. On initial evaluation we might assume that simply doing a 'Class::member' thing is enough; it is not. Qt allows signals to be overloaded and we want to retain this ability; if we tried to just do the naive member specification we would get ambiguous reference errors if the signal is overloaded. We need to perform a cast (which is why instantiating siginfo directly in our macro may not be possible by the standard):

#define FUNPTR(Object, FunctionName, Params) \
[Object]() -> void ( std::remove_pointer<decltype(Object)>::type:: *) Params \
{ \
struct helper : std::remove_pointer<decltype(Object)>::type \
{ \
typedef std::remove_pointer<decltype(Object)>::type obj_t; \
typedef void (obj_t::*ftype) Params; \
static ftype fetch() { return & helper :: FunctionName ; } \
}; \
return static_cast<decltype(helper::fetch())>(nullptr); \
}()

The first thing you'll note when looking at this definition is that it seems absurd, and it is but it's also absolutely necessary if we want to keep type safety. We could just cast nullptr to the appropriate type, because we've constructed that type from the parameters of the macro, but then user of the finished product could attempt connecting to a signal that doesn't exist, and that's something that we wished to avoid. The other issue here is that we can't just grab the address of the signal (even though it IS a function) because the preprocessor symbol 'signals' equates to 'protected', thus all Qt signals are protected. The trickery here forces the compiler to verify that the function exists with that signature so that the user can't mess up in the manner we're trying to fix.

We're not breaking the protected interface because we don't actually return the real pointer. The effect of returning the nullptr cast to the appropriate type allows us to do the examination of that type in the functions discussed above because we can pass it off as a variable to make_siginfo. The effect of doing so from this function cases misuse to result in something like this error:

main.cpp(19): error C2298: 'return' : illegal operation on pointer to member function expression

You could improve on this by using static_assert.

The other thing to note is that this is why there has to be a comma in the signature. There's no way to turn 'f(params)' into the two bits 'f' and '(params)' and as you can see, they have to be used separately in this line. We can, on the other hand, put the two bits together to use SIGNAL to create our siginfo:

#define SIGINFO(Object, FunctionName, Params) \
make_siginfo(Object \
, FUNPTR(Object,FunctionName,Params) \
, SIGNAL(FunctionName Params))

Concatenation would be preferred to turn "fun_name" and "(par,ams)" into "fun_name(par,ams)" but one, commonly used compiler, complains about it. Luckily Qt will still respond correctly if there's the space between function name and parameter list.

Very little remains now. Remember our sig_convert class from the last article? We simply need a constructor to build it:

template < typename Signal >
struct sig_convert
{
template < SigInfo, typename Fun >
sig_convert(SigInfo info, Fun f)
// : QObject(info.object) <- what we'd do in the real version.
{
static_assert( std::is_same<Signature, typename SigInfo::signature>::value
, "Signatures do not exactly match.");

// set the fwd function and attach the signal (see pt1 and pt2)
}
};

We could just make the SigInfo the parameter for sig_convert, but doing it this way allows similar signals to share code.

The very last bit is to make 'connect_static'. Cool thing here is that it doesn't have to be a MACRO:

template < typename SigInfo, typename Function >
sig_convert<typename SigInfo::signature> connect_static(SigInfo info, Function f)
{
return sig_convert<typename SigInfo::signature>(info,f);
}

In our real version we would want to return a pointer and the constructor for sig_convert would take our signal object as its parent in order to tie its lifetime with that object. This is how usual use would warrant us to do it.

Of course, because connect_static is supplying the same signature that the sig_convert then checks against, we know it'll always pass. It might seem logical to then remove that check. I'm not going to though because I envision it being possible that I'd want to use sig_convert directly when the assumptions around connect_static are not valid for some reason.

As was mentioned earlier, these particular bits of the solution only work because and when the signal you're connecting to was made by MOC and thus exists as a member function of the class you're connecting to. If you're working with something unusual that doesn't meet this requirement you can still use the bits from pt1 and pt2 but you won't get back the type safety; you'll have to do it exactly by hand. Under such conditions it may be better to simply use the Qt slot mechanism, which does the dynamic stuff pretty well and should be safer, but you could still use these objects if you're careful.

Also, the versions in these articles have all assumed a void returning slot. The last entry of this series will discuss how to add the ability to connect to non-void signals. The issue in that case isn't going to be so much implementing the invoker, because that's easy, it will be in the analysis of a protected function when you don't have protected access.

2 comments:

  1. Shouldn't &helper::Fun be &helper::FunctionName? And then in the SIGINFO macro, you shouldn't use the token-pasting operator ## because function name is properly a separate token from its parameters.

    ReplyDelete
  2. Where is the next part of this post ?

    ReplyDelete