Template wizardry for QObject::connect-like method needed.


  • Qt Champions 2016

    Hey guys,
    I've been refactoring some code in my QtMpi module and I'm a bit stuck with my API. What I need is to have a method similar to QObject::connect that will allow me to connect a specific message type reception to a pointer to member or a functor. However I'm struggling with the actual implementation of it. Now I've copied part of the code from Qt which partially works for me, but I'm erring somewhere because it can't match an implementation if I pass it a lambda. The code I currently have is here (bitbucket snippet). Now what it's supposed to do:

    • I get a message over OpenMPI's API
    • I recreate the correct class through Qt's meta type system
    • A notifier object raises a signal with one argument - the message (through the base class' pointer wrapped in a QSharedPointer), messages are derived from a common base class.

    Up to here there's no problem - I have implemented that part.
    Now what I want from there is to have a method (I called it bind) that takes one template parameter which is the message type and makes a QObject::connect such that when that kind of message arrives I get the method/lambda called with the appropriate argument (or no argument). So in the end I want to write something like this:

    bind<QMpiQuitMessage>(qApp, &QCoreApplication::quit);
    bind<QMpiQuitMessage>(qApp, [] (const QMpiQuitMessage &) -> void {
        return;
    });
    

    The actual notifier object is defined as:

    class Q_MPI_EXPORT QMpiMessageNotifier : public QObject
    {
        Q_OBJECT
        Q_DISABLE_COPY(QMpiMessageNotifier)
    
    public:
        using QObject::QObject;
    
    signals:
        void received(const QSharedPointer<QMpiMessage> &);
    };
    

    and implementation of the bind should take care to cast the const QSharedPointer<QMpiMessage> & into the correct message type at the appropriate time. Currently I do this:

    template <class MessageType, class Slot>
    inline typename std::enable_if<int(QtPrivate::FunctionPointer<Slot>::ArgumentCount) == 1 &&
    QtPrivate::FunctionPointer<Slot>::IsPointerToMemberFunction, QMetaObject::Connection>::type QMpiCommunicator::bind(const typename QtPrivate::FunctionPointer<Slot>::Object * receiver, Slot slot, Qt::ConnectionType type)
    {
        static_assert(std::is_base_of<QMpiMessage, MessageType>::value, "All MPI message classes must derive from QMpiMessage");
        return QObject::connect(notifier(QtMpi::messageType<MessageType>()), &QMpiMessageNotifier::received, receiver, [receiver, slot] (const QSharedPointer<QMpiMessage> & message) -> void {
            (const_cast<typename QtPrivate::FunctionPointer<Slot>::Object *>(receiver)->*slot)(reinterpret_cast<const MessageType &>(*message.data()));
        }, type);
    }
    

    Which compiles for the pointer to member that accepts one argument, but if I pass a lambda none of my templates matches (all of them are visible in the snippet I linked above).

    Suggestions, ideas and comments would be highly appreciated.



  • 1 clarification, what do you mean by receiver in the context of a lambda?
    bind<QMpiQuitMessage>(qApp, &QCoreApplication::quit); is clear, you want to call qApp->quit(); but what do you mean by bind<QMpiQuitMessage>(qApp, [] (const QMpiQuitMessage &) -> void {});? how should qApp be used here?


  • Qt Champions 2016

    @VRonin said in Template wizardry for QObject::connect-like method needed.:

    what do you mean by receiver in the context of a lambda?

    Exactly the same meaning as with QObject::connect - the QObject in whose event loop the lambda is to be executed (the context parameter in QObject::connect).



  • ok, so can you use what QObject::connect uses? You are using QtPrivate already so it shouldn't be much of a burden.

    basically this QtPrivate::FunctionPointer<Slot>::IsPointerToMemberFunction covers the slot case, !QtPrivate::FunctionPointer<Slot>::IsPointerToMemberFunction covers the context,functor case

    template <class MessageType, class Func1>
    inline typename std::enable_if<int(QtPrivate::FunctionPointer<Slot>::ArgumentCount) == 1 &&
    !QtPrivate::FunctionPointer<Func1>::IsPointerToMemberFunction, QMetaObject::Connection>::type QMpiCommunicator::bind(const QObject *context, Func1 slot, Qt::ConnectionType type)
    {
        static_assert(std::is_base_of<QMpiMessage, MessageType>::value, "All MPI message classes must derive from QMpiMessage");
        return QObject::connect(notifier(QtMpi::messageType<MessageType>()), &QMpiMessageNotifier::received, context, [slot] (const QSharedPointer<QMpiMessage> & message) -> void {
            std::bind(slot,reinterpret_cast<const MessageType &>(*message.data())();
        }, type);
    }
    

    EDIT

    I see in your snippet you already did this


  • Qt Champions 2016

    Yes, I think that the problem is I have std::enable_if for the functor variant, which would mean there's no std::enable_if<...>::type defined if the condition's false. This corresponds to the error I'm getting, but I'm unsure how to proceed so I can do the typecast when I have a functor with one argument and just ignore it when I have a functor with no arguments.



  • Ok, now I got it. the link I posted above had the answer:

    //Connect a signal to a pointer to qobject member function
        template <class MessageType,typename Func1, typename Func2>
        QMetaObject::Connection bind(const typename QtPrivate::FunctionPointer<Func1>::Object *sender, Func1 signal,
                                         const typename QtPrivate::FunctionPointer<Func2>::Object *receiver, Func2 slot,
    Qt::ConnectionType type = Qt::AutoConnection)
    
    //connect to a function pointer  (not a member)
        template <class MessageType,typename Func1, typename Func2>
        typename std::enable_if<int(QtPrivate::FunctionPointer<Func2>::ArgumentCount) >= 0, QMetaObject::Connection>::type
    bind(const typename QtPrivate::FunctionPointer<Func1>::Object *sender, Func1 signal, Func2 slot)
    
    //connect to a function pointer  (not a member)
        template <class MessageType,typename Func1, typename Func2>
       typename std::enable_if<int(QtPrivate::FunctionPointer<Func2>::ArgumentCount) >= 0 &&
                                              !QtPrivate::FunctionPointer<Func2>::IsPointerToMemberFunction, QMetaObject::Connection>::type
                bind(const typename QtPrivate::FunctionPointer<Func1>::Object *sender, Func1 signal, const QObject *context, Func2 slot,
    Qt::ConnectionType type = Qt::AutoConnection)
    

    Basically you are restricting too much the first case which triggers the error. QtPrivate::FunctionPointer<Func2>::Object* is what actually resolves the overload in the correct way (i.e. prevents context,functor to bind to the first version)

    EDIT:

    to discriminate between 1 and 0 arguments you can just use, in the body,

    #if int(QtPrivate::FunctionPointer<Func2>::ArgumentCount) == 0
    
    #elif  int(QtPrivate::FunctionPointer<Func2>::ArgumentCount) == 1
    
    #else
    static_assert(false,"the slot can't have more than 1 argument");
    #endif
    

  • Qt Champions 2016

    Okay, using:

        //Connect a signal to a pointer to qobject member function
        template <class MessageType, typename Slot>
        typename std::enable_if<QtPrivate::FunctionPointer<Slot>::IsPointerToMemberFunction, QMetaObject::Connection>::type bind(const typename QtPrivate::FunctionPointer<Slot>::Object * receiver, Slot slot, Qt::ConnectionType type = Qt::AutoConnection)
        {
        }
    
        //connect to a function pointer  (not a member)
        template <class MessageType, typename Functor>
        typename std::enable_if<!QtPrivate::FunctionPointer<Functor>::IsPointerToMemberFunction, QMetaObject::Connection>::type bind(Functor slot)
        {
            return bind<MessageType, Functor>(this, slot, Qt::DirectConnection);
        }
    
        //connect to a function pointer  (not a member)
        template <class MessageType, typename Functor>
        typename std::enable_if<!QtPrivate::FunctionPointer<Functor>::IsPointerToMemberFunction, QMetaObject::Connection>::type bind(const QObject * context, Functor slot, Qt::ConnectionType type = Qt::AutoConnection)
        {
        }
    

    Matches and compiles. However the second part - with the preprocessor - seems rather odd. I don't see how it'd work (and g++ agrees).



  • @kshegunov said in Template wizardry for QObject::connect-like method needed.:

    However the second part - with the preprocessor - seems rather odd. I don't see how it'd work (and g++ agrees).

    I thought the pre-processor was smart enough. It might sound "dirty" then but I'd just use a if/elseif or switch in the body and let the compiler optimise out the part that will never be executed. This is one of the cases described in your post where you'd do constexpr injection



  • Actually I have a better solution:

    // private:
    template <class MessageType, typename Functor, 1>
        typename std::enable_if<!QtPrivate::FunctionPointer<Functor>::IsPointerToMemberFunction, QMetaObject::Connection>::type bind(const QObject * context, Functor slot, Qt::ConnectionType type = Qt::AutoConnection)
        {
    // 1 argument
        }
    template <class MessageType, typename Functor, 0>
        typename std::enable_if<!QtPrivate::FunctionPointer<Functor>::IsPointerToMemberFunction, QMetaObject::Connection>::type bind(const QObject * context, Functor slot, Qt::ConnectionType type = Qt::AutoConnection)
        {
    // 0 arguments
        }
    // public:
    template <class MessageType, typename Functor>
        typename std::enable_if<!QtPrivate::FunctionPointer<Functor>::IsPointerToMemberFunction, QMetaObject::Connection>::type bind(const QObject * context, Functor slot, Qt::ConnectionType type = Qt::AutoConnection)
        {
    return bind<MessageType, Functor, int(QtPrivate::FunctionPointer<Func2>::ArgumentCount) >(context,slot,type);
        }
    

    P.S.
    I suspect the condition int(QtPrivate::FunctionPointer<Func2>::ArgumentCount) >= 0 put in by Qt's dev has a reason to live (even though I can't think of one at the moment) so I would not trash it just yet


  • Qt Champions 2016

    @VRonin said in Template wizardry for QObject::connect-like method needed.:

    I thought the pre-processor was smart enough.

    At the time it runs there's no notion of C++ code, so it can't know what the hell we're trying to feed it ... ;)

    This is one of the cases described in your post where you'd do constexpr injection

    Yeah, I sp'ose that's true. I'll try to make it work from here.

    @VRonin said in Template wizardry for QObject::connect-like method needed.:

    Actually I have a better solution

    Yes, that was what I was thinking of too. It's called tagging or something akin.

    I can't think of one at the moment

    QtPrivate::FunctionPointer<Func2>::ArgumentCount == -1 means unknown number of arguments (for whatever reason)


  • Qt Champions 2016

    After dissecting Qt's QObject::connect implementation thoroughly I can say I have now a much better understanding of what is going on. It's a mess out there folks, the amount of instantiations for each one connect call is just mind-boggling. Anyway, the problem is now solved. One needs 4 distinct implementations to support what I want to have, one each for pointer to members, global/static functions with context object, functors (lambdas and such) with context object and one for function-like objects without context.

    In the end I had to do some special substitutions to fool the compatibility checks but ultimately the code compiles and matches correctly now. Just for completeness here's what I came up:

    // Bind to pointer-to-members
    template <class MessageType, typename Slot>
    typename QMetaObject::Connection bind(const typename QtPrivate::FunctionPointer<Slot>::Object * receiver, Slot slot, Qt::ConnectionType type = Qt::AutoConnection)
    {
        typedef QtPrivate::FunctionPointer<decltype(&QMpiMessageNotifier::received)> SignalType;
        typedef QtPrivate::FunctionPointer<Slot> SlotType;
    
        typedef QtPrivate::List<const MessageType &> SignalArguments;
    
        // Check the compatibility of arguments (from Qt's source).
        Q_STATIC_ASSERT_X(int(SignalType::ArgumentCount) >= int(SlotType::ArgumentCount), "The slot requires more arguments than the signal provides.");
        Q_STATIC_ASSERT_X((QtPrivate::CheckCompatibleArguments<SignalArguments, typename SlotType::Arguments>::value), "Signal and slot arguments are not compatible.");
        Q_STATIC_ASSERT_X((QtPrivate::AreArgumentsCompatible<typename SlotType::ReturnType, SignalType::ReturnType>::value), "Return type of the slot is not compatible with the return type of the signal.");
    
        return QObject::connect(notifier(QtMpi::messageType<MessageType>()), &QMpiMessageNotifier::received, receiver, [slot] (const QSharedPointer<QMpiMessage> & message) -> void {
            QMpiMessage * msg = message.data();
            SlotType::template call<typename SlotType::Arguments, typename SlotType::ReturnType>(slot, nullptr, reinterpret_cast<void **>(&msg));
        } , type);
    }
    
    // Bind to static/global functions with a context object
    template <class MessageType, typename Functor>
    typename std::enable_if<QtPrivate::FunctionPointer<Functor>::ArgumentCount >= 0 && !QtPrivate::FunctionPointer<Functor>::IsPointerToMemberFunction, QMetaObject::Connection>::type bind(const QObject * context, Functor slot, Qt::ConnectionType type = Qt::AutoConnection)
    {
        typedef QtPrivate::FunctionPointer<decltype(&QMpiMessageNotifier::received)> SignalType;
        typedef QtPrivate::FunctionPointer<Functor> SlotType;
    
        typedef QtPrivate::List<const MessageType &> SignalArguments;
    
        // Check the compatibility of arguments (from Qt's source).
        Q_STATIC_ASSERT_X(int(SignalType::ArgumentCount) >= int(SlotType::ArgumentCount), "The slot requires more arguments than the signal provides.");
        Q_STATIC_ASSERT_X((QtPrivate::CheckCompatibleArguments<SignalArguments, typename SlotType::Arguments>::value), "Signal and slot arguments are not compatible.");
        Q_STATIC_ASSERT_X((QtPrivate::AreArgumentsCompatible<typename SlotType::ReturnType, typename SignalType::ReturnType>::value), "Return type of the slot is not compatible with the return type of the signal.");
    
        return QObject::connect(notifier(QtMpi::messageType<MessageType>()), &QMpiMessageNotifier::received, context, [slot] (const QSharedPointer<QMpiMessage> & message) -> void {
            QMpiMessage * msg = message.data();
            QtPrivate::FunctionPointer<Functor>::template call<typename QtPrivate::FunctionPointer<Functor>::Arguments, void>(slot, nullptr, reinterpret_cast<void **>(&msg));
        }, type);
    }
    
    // Bind to functors (lambdas included) with a context object
    template <class MessageType, typename Functor>
    typename std::enable_if<QtPrivate::FunctionPointer<Functor>::ArgumentCount == -1, QMetaObject::Connection>::type bind(const QObject * context, Functor slot, Qt::ConnectionType type = Qt::AutoConnection)
    {
        typedef QtPrivate::List<const MessageType &> SignalArguments;
    
        const int FunctorArgumentCount = QtPrivate::ComputeFunctorArgumentCount<Functor, SignalArguments>::Value;
    
        Q_STATIC_ASSERT_X((FunctorArgumentCount >= 0), "Signal and slot arguments are not compatible.");
    
        const int SlotArgumentCount = (FunctorArgumentCount >= 0) ? FunctorArgumentCount : 0;
        auto functor = new QtPrivate::QFunctorSlotObject<Functor, SlotArgumentCount, typename QtPrivate::List_Left<SignalArguments, SlotArgumentCount>::Value, void>(slot);
    
        return QObject::connect(notifier(QtMpi::messageType<MessageType>()), &QMpiMessageNotifier::received, context, [functor] (const QSharedPointer<QMpiMessage> & message) -> void {
            QMpiMessage * msg = message.data();
            functor->call(nullptr, reinterpret_cast<void **>(&msg));
        }, type);
    }
    
    // Bind to functors or functions (no context object)
    template <class MessageType, typename Functor>
    typename QMetaObject::Connection bind(Functor slot)
    {
        return bind<MessageType, Functor>(this, slot, Qt::DirectConnection);
    }
    

    A big thanks to Luca for playing along, and I hope this may be useful to someone in the future.

    If you have any questions that I can answer, don't hesitate to ask.


Log in to reply
 

Looks like your connection to Qt Forum was lost, please wait while we try to reconnect.