I am trying to use coroutines with Qt. Here's minimal(I guess) example to reproduce my problem Basically, bellow code is adopted from the example of cppreference here: https://en.cppreference.com/w/cpp/coroutine/noop_coroutine.html
#include <QtCore/QCoreApplication>
#include <QtCore/QTimer>
#include <coroutine>
#include <utility>
template<class T>
struct task
{
struct promise_type
{
auto get_return_object()
{
return task(std::coroutine_handle<promise_type>::from_promise(*this));
}
std::suspend_always initial_suspend() { return {}; }
struct final_awaiter
{
bool await_ready() noexcept { return false; }
void await_resume() noexcept {}
std::coroutine_handle<>
await_suspend(std::coroutine_handle<promise_type> h) noexcept
{
if (auto previous = h.promise().previous; previous)
return previous;
else
return std::noop_coroutine();
}
};
final_awaiter final_suspend() noexcept { return {}; }
void unhandled_exception() { throw; }
void return_value(T value) { result = std::move(value); }
T result;
std::coroutine_handle<> previous;
};
task(std::coroutine_handle<promise_type> h) : coro(h) {}
task(task&& t) = delete;
~task() { coro.destroy(); }
struct awaiter
{
bool await_ready() { return false; }
T await_resume() { return std::move(coro.promise().result); }
auto await_suspend(std::coroutine_handle<> h)
{
coro.promise().previous = h;
return coro;
}
std::coroutine_handle<promise_type> coro;
};
awaiter operator co_await() { return awaiter{coro}; }
T operator()()
{
coro.resume();
return std::move(coro.promise().result);
}
private:
std::coroutine_handle<promise_type> coro;
};
class qt_timeout {
int m_delay = 0;
public:
qt_timeout(int delay) noexcept : m_delay{delay} {}
auto await_ready() const noexcept { return false; }
auto await_suspend(std::coroutine_handle<> h) noexcept
{ QTimer::singleShot(m_delay, qApp, [h] { h.resume(); }); }
auto await_resume() noexcept {}
};
auto timeout(int ms) -> task<int>
{
co_await qt_timeout(ms);
co_return ms;
}
auto main(int argc, char **argv) -> int
{
QCoreApplication app{argc, argv};
QMetaObject::invokeMethod(&app, [] {
auto t = timeout(1000);
const auto r = t();
qDebug() << "timeout" << r;
}, Qt::QueuedConnection);
return app.exec();
}
This example tries resume coroutine in Qt's event loop. I have expected "timeout 1000" is printed after about 1000ms. However, it prints "timeout 0" and exits abnormally.
How can I fix it?
Edited:
I have simply implemented qt_task from the code snippet from Ahmed AEK's answer. At first it looked working, but actually it doesn't:
#include <QtCore/QCoreApplication>
#include <QtCore/QPointer>
#include <QtCore/QTimer>
#include <coroutine>
#include <utility>
struct qt_task
{
struct promise_type
{
auto get_return_object()
{
return qt_task(std::coroutine_handle<promise_type>::from_promise(*this));
}
std::suspend_always initial_suspend() { return {}; }
struct final_awaiter
{
bool await_ready() noexcept { return false; }
void await_resume() noexcept {}
std::coroutine_handle<>
await_suspend(std::coroutine_handle<promise_type> h) noexcept
{
if (auto previous = h.promise().previous; previous)
{
return previous;
}
else
{
h.promise().parent_object->deleteLater();
return std::noop_coroutine();
}
}
};
final_awaiter final_suspend() noexcept { return {}; }
void unhandled_exception() { throw; }
void return_void() { }
std::coroutine_handle<> previous;
QObject* parent_object = nullptr;
};
qt_task(std::coroutine_handle<promise_type> h) : coro(h) {}
qt_task(qt_task&& t)
: coro{ std::exchange(t.coro, std::coroutine_handle<promise_type>{}) }
{
}
qt_task& operator=(qt_task&& t)
{
std::ranges::swap(coro, t.coro);
return *this;
}
~qt_task() {
if (coro)
{
coro.destroy();
}
}
struct awaiter
{
bool await_ready() { return false; }
void await_resume() { }
template <typename U>
auto await_suspend(std::coroutine_handle<U> h)
requires requires { h.promise().parent_object; }
{
coro.promise().previous = h;
coro.promise().parent_object = h.promise().parent_object;
return coro;
}
std::coroutine_handle<promise_type> coro;
};
awaiter operator co_await() { return awaiter{ coro }; }
friend void dispatch(QObject* parent, qt_task t);
private:
std::coroutine_handle<promise_type> coro;
};
class QAsyncTask : public QObject
{
public:
QAsyncTask(qt_task task, QObject* parent)
:QObject{parent}, m_task{std::move(task)}
{
}
qt_task m_task;
};
class qt_timeout {
int m_delay = 0;
public:
qt_timeout(int delay) noexcept : m_delay{ delay } {}
auto await_ready() const noexcept { return false; }
template <typename T>
auto await_suspend(std::coroutine_handle<T> h) noexcept
requires requires { h.promise().parent_object; }
{ QTimer::singleShot(m_delay, h.promise().parent_object, [h] { h.resume(); }); }
auto await_resume() noexcept {}
};
inline void dispatch(QObject* parent, qt_task t)
{
auto qtask = new QAsyncTask(std::move(t), parent);
qtask->m_task.coro.promise().parent_object = qtask;
qtask->m_task.coro.resume();
}
auto main(int argc, char** argv) -> int
{
QCoreApplication app{ argc, argv };
QMetaObject::invokeMethod(&app, [&app] {
dispatch(&app, [t=1] -> qt_task {
co_await qt_timeout(1000);
qDebug() << "done";
qDebug() << t;
}());
}, Qt::QueuedConnection);
return app.exec();
}
I have made qt_task only for void for simplicity. The result should be obviously "done\n1" but it prints garbage value for t.
I guess somehow the captured variable is destroyed or its memory is corrupted.
How can I fix it, again?
qcoro implements all the necessary boilerplate so you don't have to do it, i wouldn't recommend implementing your own tasks type anyway.
your application actually segfaults, you have 2 problems
task from the function and co_await the timeout object for this to be considered a coroutine by C++.task, Qt won't handle it so you need to handle it yourself, remember tasks are suspendable, they need to live somewhere until they are finished.To handle the task's lifetime, you need bind it to a specific QObject or do reference counting as qcoro does, so that it outlives the stack frame and when the widget is destroyed the coroutine can't be resumed anymore, people will create a QObject just for holding this task, but we don't need to do it for this example, in the next example parent_object guarantees the coroutine can't resume after its parent is destroyed.
#include <QtCore/QCoreApplication>
#include <QtCore/QTimer>
#include <coroutine>
#include <utility>
#include <iostream>
template<class T>
struct task
{
struct promise_type
{
auto get_return_object()
{
return task(std::coroutine_handle<promise_type>::from_promise(*this));
}
std::suspend_always initial_suspend() { return {}; }
struct final_awaiter
{
bool await_ready() noexcept { return false; }
void await_resume() noexcept {}
std::coroutine_handle<>
await_suspend(std::coroutine_handle<promise_type> h) noexcept
{
if (auto previous = h.promise().previous; previous)
{
return previous;
}
else
{
return std::noop_coroutine();
}
}
};
final_awaiter final_suspend() noexcept { return {}; }
void unhandled_exception() { throw; }
void return_value(T value) { result = std::move(value); }
T result;
std::coroutine_handle<> previous;
QObject* parent_object = nullptr;
};
task(std::coroutine_handle<promise_type> h) : coro(h) {}
task(task&& t)
: coro{ std::exchange(t.coro, std::coroutine_handle<promise_type>{}) }
{
}
task& operator=(task&& t)
{
std::ranges::swap(coro, t.coro);
return *this;
}
~task() {
if (coro)
{
coro.destroy();
}
}
struct awaiter
{
bool await_ready() { return false; }
T await_resume() { return std::move(coro.promise().result); }
template <typename U>
auto await_suspend(std::coroutine_handle<U> h)
requires requires { h.promise().parent_object; }
{
// templated so that task<double> can await task<int>
coro.promise().previous = h;
coro.promise().parent_object = h.promise().parent_object;
return coro;
}
std::coroutine_handle<promise_type> coro;
};
awaiter operator co_await() { return awaiter{ coro }; }
void dispatch(QObject* parent)
{
coro.promise().parent_object = parent;
coro.resume();
}
private:
std::coroutine_handle<promise_type> coro;
};
class qt_timeout {
int m_delay = 0;
public:
qt_timeout(int delay) noexcept : m_delay{ delay } {}
auto await_ready() const noexcept { return false; }
template <typename T>
auto await_suspend(std::coroutine_handle<T> h) noexcept
requires requires { h.promise().parent_object; }
{ QTimer::singleShot(m_delay, h.promise().parent_object, [h] { h.resume(); }); }
auto await_resume() noexcept {}
};
inline auto timeout(int ms) -> task<int>
{
co_await qt_timeout(ms);
co_return ms;
}
inline auto wait_task() -> task<double>
{
auto result1 = co_await timeout(1000);
std::cout << "done1! " << result1 << "\n";
auto result2 = co_await timeout(1500);
std::cout << "done2! " << result2 << "\n";
auto result3 = co_await timeout(2000);
std::cout << "done3! " << result3 << "\n";
QCoreApplication::quit();
co_return 1.0;
}
auto main(int argc, char** argv) -> int
{
QCoreApplication app{ argc, argv };
auto my_task = wait_task();
my_task.dispatch(&app); // replace it with the object that owns this task
return app.exec();
}
done1! 1000
done2! 1500
done3! 2000
instead of dispatch directly calling resume it can queue it with invokeMethod but this adds an unnecessary overhead, you usually find 2 methods in executors for this.
void post(QObject* parent)
{
coro.promise().parent_object = parent;
QMetaObject::invokeMethod(parent, [coro = coro] {
coro.resume();
}, Qt::QueuedConnection);
}
void dispatch(QObject* parent)
{
coro.promise().parent_object = parent;
coro.resume();
}
now for the task lifetime, how do you delete the task after the coroutine is done ? you can delete the task in the await_suspend of the final awaiter.
inside dispatch or post you can create a QObject (let's call it QAsyncTask) that stores Task<void>, and whose parent is your original QObject, then in final_awaiter you can call deleteLater() in the case there is no parent promise, sadly this causes a few allocations per top-level coroutine, note that final_suspend can be called during the destructor of the object, so we cannot delele the object in it or it will cause a double free, and must post the object destruction or we can detect that the parent destructor has started and not delete the parent, there is a small room for optimization here.
// in final_awaiter
await_suspend(std::coroutine_handle<promise_type> h) noexcept
{
if (auto previous = h.promise().previous; previous)
{
return previous;
}
else // we are toplevel, we need to delete the parent QAsyncTask
{
h.promise().parent_object->deleteLater();
return std::noop_coroutine();
}
}
friend void dispatch(QObject* parent, task<void> t) // in task
{
auto qtask = new QAsyncTask(std::move(t), parent);
qtask->m_task.coro.promise().parent_object = qtask;
qtask->m_task.coro.resume();
}
class QAsyncTask : public QObject
{
public:
Q_OBJECT
QAsyncTask(task<void> task, QObject* parent)
:QObject{parent}, m_task{std::move(task)}
{
}
task<void> m_task;
};
this code won't compile right now because you need to specialize the promise for void, you'll also want to log any exception that happened to the terminal instead of throwing out of unhandled_exception because that will be thrown into Qt and terminate the application.
qcoro instead uses reference counting in the promise, where 1 reference is in the task and another reference is decremented in the final_suspend awaiter, this leads to memory and object leaks if the coroutine doesn't finish, and as the coroutine is unrelated to the lifetime of a QObject you could use the object inside the coroutine after the object is destroyed and crash, and cancellation is a little harder, qcoro currently has a few open issues because of this.