qcoro icon indicating copy to clipboard operation
qcoro copied to clipboard

Add lifetime docs for possible footguns

Open kelteseth opened this issue 1 year ago • 1 comments

QCoro is an amazing library that I heavily rely on (ScreenPlay), but I would like to propose some sort of known pitfalls like I encountered when using emits after co_await:

QCoro::QmlTask CppBackend::test()
{
    // ⚠️ This will crash:
    // This crashes because the lambda captures this by value,
    // but the lifetime of the coroutine extends beyond the function
    // call. When longFunction() is co_awaited, the coroutine is
    // suspended, and control returns to the caller.
    // The QmlTask object is then returned, but the lambda
    // (and thus the captured this) goes out of scope. When the coroutine
    // resumes later, it tries to use the now-invalid this pointer, causing a crash.
    return QCoro::QmlTask([this]() -> QCoro::Task<Result> {
        co_await longFunction();
        emit testCallSignal();
        co_return Result{};
    }());

    // ✅ This will _not_ crash
    // This doesn't crash because the emit testCallSignal() is executed within
    // the then() callback, which is called immediately after longFunction() completes.
    // The this pointer is still valid at this point.
    return QCoro::QmlTask([this]() -> QCoro::Task<Result> {
        co_await longFunction().then([this]() { emit testCallSignal(); });
        co_return Result{};
    }());

    // ✅ This will _not_ crash
    // This example also doesn't crash for the same reason as the previous one.
    // All emit testCallSignal() calls are within then() callbacks, which are executed
    // immediately after their respective longFunction() calls complete.
    return QCoro::QmlTask([this]() -> QCoro::Task<Result> {
        co_await longFunction().then([this]() -> QCoro::Task<Result> {
            emit testCallSignal();
            co_await longFunction().then([this]() { emit testCallSignal(); });
            co_return Result{};
        });
        co_return Result{};
    }());

    // ⚠️ This will crash:
    // This case crashes for the same reason as the first crashing example.
    // Although each longFunction().then() call is safe, the coroutine is suspended
    // between these calls. The this pointer captured by the outer lambda may become
    // invalid during these suspensions, leading to a crash when the coroutine resumes.
    return QCoro::QmlTask([this]() -> QCoro::Task<Result> {
        co_await longFunction().then([this]() { emit testCallSignal(); });
        co_await longFunction().then([this]() { emit testCallSignal(); });
        co_await longFunction().then([this]() { emit testCallSignal(); });
        co_return Result{};
    }());
}

You can copy this into the docs if you like :)

Example project Footguns.zip

kelteseth avatar Aug 17 '24 09:08 kelteseth

Hi, thanks for the feedback, you are right the docs are not very good in this regard.

Thanks for the examples, I'll write something down and use your examples.

danvratil avatar Aug 19 '24 07:08 danvratil