c++qtqmetaobject

Functionpointer based syntax for QMetaObject invokeMethod


I'm working on a simple wrapper for a IPC lib we are using.

I want to convert the events from this lib to calls on Qt slots.

Right now i have something like this:

void Caller::registerCallback(int id, QObject* reciever, const char* member)
{
    _callbackMap[id] = std::make_pair(reciever, QString(member));
}

bool Caller::call(const SomeData data)
{
    auto reciever = _callbackMap.value(data.id);

    return QMetaObject::invokeMethod(reciever.first, reciever.second.toLocal8Bit(), Qt::QueuedConnection, 
        QGenericReturnArgument(),
        Q_ARG(SomeData, data));
}
void Receiver::someCallback(SomeData data)
{
    qDebug() << data.str;
}
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    Caller caller;
    Receiver reciever;

    caller.registerCallback(1, &reciever, "someCallback");

    caller.call(SomeData({ "Hi", 1 }));

    return a.exec();
}
struct SomeData {
    QString str;
    int id;
}; Q_DECLARE_METATYPE(SomeData);

This works quite well. But I don't like to register the callbacks as strings. I would prefer a compile time checking with a syntax like this:

caller.registerCallback(1, &reciever, &Reciever::someCallback);

I am aware of this implementation.

The slots I want to register always have exactly one argument and no return value.

I already found this request what could solve my problem but unfortunately this was never implemented. Also this question doesn't help me as I'm not able to patch the moc we are using.

So is this really not possible with all the meta magic Qt is using?


EDIT:

I found a solution that works also when the Caller dose not know about the Receiver (what is actually what I need):

//Caller.h

class Caller : public QObject
{
    Q_OBJECT

public:
    Caller(QObject *parent = nullptr);
    ~Caller();

    //void registerCallback(int id, QObject* reciever, const char *member);
    template < class R, typename Func >
    void inline registerCallback(int id, R reciever, Func callback)
    {
        using std::placeholders::_1;
        registerCallbackImpl(id, reciever, std::bind(callback, reciever, _1));
    };

    bool call(const SomeData);

private:
    QMap<int, std::pair<QObject *, std::function<void(SomeData)>> > _callbackMap;

    void registerCallbackImpl(int id, QObject* reciever, std::function<void(SomeData)> callback);
};
//Caller.cpp
void Caller::registerCallbackImpl(int id, QObject* reciever, std::function<void(SomeData)> callback)
{
    _callbackMap[id] = std::make_pair(reciever, callback);
}

bool Caller::call(const SomeData data)
{
    auto reciever = _callbackMap.value(data.id).first;
    auto fn = _callbackMap.value(data.id).second;

    QMetaObject::invokeMethod(reciever, [reciever, fn, data]() {
        std::invoke(fn, data);
        fn(data);
        }, Qt::QueuedConnection);

    return true;
}

//main.cpp

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    Caller caller;
    Receiver reciever;

    using std::placeholders::_1;

    caller.registerCallback(2, &reciever, &Receiver::someCallback);

    caller.call(SomeData({ "Hi2", 2 }));

    return a.exec();
}

Solution

  • This soulution relies upon std::invoke and lambda.

    Variant 1: use std::invoke directly instead of QMetaObject::invoke

    Variant 2: use std::invoke inside a lambda, which is passed to QMetaObject::invoke

    Variant 3: use MACRO instead of std::invoke in variant 2.

    If you use QMetaObject::invoke you've got an option to choose connection type - Direct or Queued. In variant 1 the call is invoked immediately like in direct connection.

    receiver.h

    #ifndef RECEIVER_H
    #define RECEIVER_H
    
    #include <QObject>
    #include <QDebug>
    
    struct SomeData {
        QString str;
        int id;
    };
    //Q_DECLARE_METATYPE(SomeData);
    
    class Receiver : public QObject
    {
        Q_OBJECT
    public:
        explicit Receiver(QObject *parent = nullptr) : QObject(parent) {}
    
        void doSmth(SomeData data) {
            qDebug() << data.str;
        }
    
    signals:
    
    };
    
    #endif // RECEIVER_H
    

    caller.h

    #ifndef CALLER_H
    #define CALLER_H
    
    #include <QObject>
    #include <QMap>
    #include <utility>
    #include <map>
    
    #include "receiver.h"
    #define CALL_MEMBER_FN(object,ptrToMember)  ((object)->*(ptrToMember))
    typedef void (Receiver::*callback)(SomeData);
    
    class Caller : public QObject
    {
        Q_OBJECT
    public:
        explicit Caller(QObject *parent = nullptr) : QObject(parent) { }
    
        void registerCallback(int id, Receiver* receiver, callback c)
        {
            auto pair = std::make_pair(receiver, c);
            _callbackMap.emplace(id, pair);
        }
    
        bool call(const SomeData data)
        {
            auto &receiver = _callbackMap.at(data.id);
            return QMetaObject::invokeMethod(receiver.first, [data, receiver] () {
                // method 1
                std::invoke(receiver.second, receiver.first, data);
                // method 2 (better not to use a MACRO)
                CALL_MEMBER_FN(receiver.first, receiver.second)(data);
            }, Qt::QueuedConnection);
        }
    
        bool call_invoke(const SomeData data)
        {
            auto &receiver = _callbackMap.at(data.id);
            std::invoke(receiver.second, receiver.first, data);
            return true;
        }
    
    signals:
    
    private:
        std::map<int,std::pair<Receiver*,callback>> _callbackMap;
    };
    
    #endif // CALLER_H
    

    main.cpp

    #include <QCoreApplication>
    
    #include "receiver.h"
    #include "caller.h"
    
    int main(int argc, char *argv[])
    {
        QCoreApplication a(argc, argv);
    
        Caller caller;
        Receiver reciever;
    
        caller.registerCallback(1, &reciever, &Receiver::doSmth);
        caller.registerCallback(2, &reciever, &Receiver::doSmth);
        caller.call(SomeData({ "Hi", 1 }));
        caller.call_invoke(SomeData({ "Hi2", 2 }));
    
        return a.exec();
    }