Qt/C++: What's the best way to call a method asynchronously in Qt event loop without having to write its name as string?

Viewed 649

The normal way is with invokeMethod:

QMetaObject::invokeMethod(this, "methodName", Qt::QueuedConnection);

which works fine, except that I don't want to write methods in strings. Like ever. Why? Because this is a refactoring nightmare. Imagine having to change a method name for some reason. The software will malfunction and I'll have to notice in stderr that the method doesn't exist.

My solution?

I use QTimer. Like this:

QTimer::singleShot(0, this, &ClassName::methodName);

which seems to work fine. What I like about this is that you can replace the &ClassName::methodName part with a lambda/bind and it'll still be bound to the correct object (in case I need to use it with QThread) with the expected variable referencing we understand in standard C++:

QTimer::singleShot(0, threadObject, [this, param1, &param2](){ this->methodName(param1, param2); });

Better solutions exist?

But... I'm hoping there's a better way to do this because someone reading my code will not understand why I'm using a QTimer here... so I gotta comment everything.

Is there a better way to do this that's compatible with versions of Qt down to 5.9 or 5.7? What's the best solution you know?

3 Answers

Actually QTimer is not an asynchronous way to solve your problem. It pushes your method onto the stack to call your method when the currently executing method exits.

I think that the better solution is to use QtConcurrent run. It will execute your method in async way.

It is simple to use it:

QtConcurrent::run(&myFunc); // some function
QtConcurrent::run(this, &MyClass::method); // for class method
QtConcurrent::run(this, &MyClass::methodWithParams, param1, param2); // for class method with params

Also, you should include concurrent to your .qmake file like this:

...
QT += concurrent
...

If you need a result of function, you can use QFuture class.

It's not much different from what you're already doing, but I've done it by connecting to the destruction of a QObject instead of using a QTimer.

template <typename Func>
void MyClass::runLater(Func &&func)
{
    QObject toBeDestroyed;
    QObject::connect(&toBeDestroyed, &QObject::destroyed, this, func, Qt::QueuedConnection);
}

And then I can call it with a lambda:

void doSomething(const QString &text, int num)
{
    runLater([text, num]() { qDebug() << text << num; });
}
Related