c++gcccoroutinecompiler-bug

Coroutine frame overridden by other coroutine? (GCC 11.3 on -O2 and higher)


I am encountering an issue with my coroutines in GCC 11.3: I have implemented an event loop, where multiple coroutines are alternatingly stepped forwards (if their awaitable is ready again). I have recently noticed that the optimized build is not behaving as expected, and I believe I have narrowed it down to the frame being somehow overridden by the other coroutine.

Here's the code: (Godbolt link)

#include <array>
#include <cstdio>
#include <coroutine>

struct task
{
    struct promise_type
    {
        auto get_return_object() -> task
        {
            return task{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        auto initial_suspend() noexcept -> std::suspend_always  { return {}; }
        auto final_suspend() noexcept -> std::suspend_always  { return {}; }
        void return_void() {}
        void unhandled_exception() {}
    };

    task(std::coroutine_handle<promise_type> handle_) : handle{handle_} {}

    std::coroutine_handle<promise_type> handle;
};

auto main(void) -> int
{
    auto a = "a";
    auto b = "b";

    std::array tasks = {
        [&]() -> task { 
            printf(a);
            co_return;
        }(),
        [&]() -> task
        {
            printf(b);
            co_return;
        }()
    };

    for (auto& task : tasks)
        task.handle.resume();
}

Code explanation

The code first defines task, a basic coroutine type. It then creates an array of two tasks (printing a and b, respecitely), and resumes their coroutine handles.

Results

The Godbolt link has three compilers configured:

Since 12.1 doesn't show the issue anymore, it looks like something got fixed. Unfortunately, I cannot update my compiler, so I am trying to understand what triggers the issue, and how to avoid it. So:

What is happening? Is this a compiler bug? How to fix this?


Solution

  • After reducing my MWE further and subsequently rephrasing my search terms, I was able to find the answer to my question: The bug is in the C++ standard, not GCC.

    What's happening is that the lambdas for the tasks are captured in the coroutine frame by reference/by pointer (as dictated by the standard, as far as I understand). Since the coroutine objects are created via an immediately invoked lambda expression, the lambda object itself goes out of scope as soon as the coroutine initially suspends (which is immediately in my case). To fix this, we have two options:

    The first option can be implemented as follows (Godbolt):

    #include <array>
    #include <cstdio>
    #include <coroutine>
    
    struct task
    {
        struct promise_type
        {
            auto get_return_object() -> task
            {
                return task{std::coroutine_handle<promise_type>::from_promise(*this)};
            }
            auto initial_suspend() noexcept -> std::suspend_always  { return {}; }
            auto final_suspend() noexcept -> std::suspend_always  { return {}; }
            void return_void() {}
            void unhandled_exception() {}
        };
    
        task(std::coroutine_handle<promise_type> handle_) : handle{handle_} {}
    
        std::coroutine_handle<promise_type> handle;
    };
    
    auto main(void) -> int
    {
        auto a = "a";
        auto b = "b";
    
        auto task_a = [&]() -> task { 
                printf(a);
                co_return;
            };
        auto task_b = [&]() -> task
            {
                printf(b);
                co_return;
            };
        std::array tasks = {
            task_a(),
            task_b()
        };
    
        for (auto& task : tasks)
            task.handle.resume();
    }
    

    Note how I am simply storing the lambdas themselves in local variables before constructing the coroutine from them. This guarantees that the lambdas are only destroyed after the coroutines are destroyed. (assuming the coroutines are completed before the function exits)