'A workaround of the crash caused by calling destroy() from final_suspend()

Two days ago in my previous post I provided a code that works with GCC but crashes with MSVC2002 that calls the task destructor two times.

Today I made it work with both MSVC2002 and GCC by replacing my former await_suspend implementation:

std::coroutine_handle<> await_suspend(std::coroutine_handle<UpdatePromise> h) noexcept
{
    // resume awaiting coroutine or if there is no coroutine to resume return special coroutine that do
    // nothing
    std::coroutine_handle<> val = awaiting_coroutine ? awaiting_coroutine : std::noop_coroutine();

    h.destroy();

    return val;
}

with the following:

void await_suspend(std::coroutine_handle<UpdatePromise> h) noexcept
{
    auto coro = awaiting_coroutine;
    
    h.destroy();

    if (coro)
    {
        coro.resume();
    }
}

What can be a difference between these two implementations?

If they are identical why are different return types of await_suspend are supported by the compiler? What are they for? Is it something like a syntax sugar?

Now the full example looks like this:

#include <coroutine>
#include <optional>

#include <iostream>
#include <thread>

#include <chrono>
#include <queue>
#include <vector>

// simple timers

// stored timer tasks
struct timer_task
{
    std::chrono::steady_clock::time_point target_time;
    std::coroutine_handle<> handle;
};

// comparator
struct timer_task_before_cmp
{
    bool operator()(const timer_task& left, const timer_task& right) const
    {
        return left.target_time > right.target_time;
    }
};

std::priority_queue<timer_task, std::vector<timer_task>, timer_task_before_cmp> timers;

inline void submit_timer_task(std::coroutine_handle<> handle, std::chrono::nanoseconds timeout)
{
    timers.push(timer_task{ std::chrono::steady_clock::now() + timeout, handle });
}

//template <bool owning>
struct UpdatePromise;

//template <bool owning>
struct UpdateTask
{
    // declare promise type
    using promise_type = UpdatePromise;

    UpdateTask(std::coroutine_handle<promise_type> handle) :
        handle(handle)
    {
        std::cout << "UpdateTask constructor." << std::endl;
    }

    UpdateTask(const UpdateTask&) = delete;

    UpdateTask(UpdateTask&& other) : handle(other.handle)
    {
        std::cout << "UpdateTask move constructor." << std::endl;
    }

    UpdateTask& operator = (const UpdateTask&) = delete;

    UpdateTask& operator = (const UpdateTask&& other)
    {
        handle = other.handle;

        std::cout << "UpdateTask move assignment." << std::endl;

        return *this;
    }

    ~UpdateTask()
    {
        std::cout << "UpdateTask destructor." << std::endl;
    }

    std::coroutine_handle<promise_type> handle;
};

struct UpdatePromise
{
    std::coroutine_handle<> awaiting_coroutine;

    UpdateTask get_return_object();

    std::suspend_never initial_suspend()
    {
        return {};
    }

    void unhandled_exception()
    {
        std::terminate();
    }

    auto final_suspend() noexcept
    {
        // if there is a coroutine that is awaiting on this coroutine resume it
        struct transfer_awaitable
        {
            std::coroutine_handle<> awaiting_coroutine;

            // always stop at final suspend
            bool await_ready() noexcept
            {
                return false;
            }

            //Results in a crash with MSVC2022, but not with GCC.
            /*
            std::coroutine_handle<> await_suspend(std::coroutine_handle<UpdatePromise> h) noexcept
            {
                // resume awaiting coroutine or if there is no coroutine to resume return special coroutine that do
                // nothing
                std::coroutine_handle<> val = awaiting_coroutine ? awaiting_coroutine : std::noop_coroutine();

                h.destroy();

                return val;
            }*/

            //Does not crash.
        void await_suspend(std::coroutine_handle<UpdatePromise> h) noexcept
        {
            auto coro = awaiting_coroutine;
            
            h.destroy();

            if (coro)
            {
                coro.resume();
            }
        }

            void await_resume() noexcept {}
        };

        return transfer_awaitable{ awaiting_coroutine };
    }

    void return_void() {}

    // use `co_await std::chrono::seconds{n}` to wait specified amount of time
    auto await_transform(std::chrono::milliseconds d)
    {
        struct timer_awaitable
        {
            std::chrono::milliseconds m_d;

            // always suspend
            bool await_ready()
            {
                return m_d <= std::chrono::milliseconds(0);
            }

            // h is a handler for current coroutine which is suspended
            void await_suspend(std::coroutine_handle<> h)
            {
                // submit suspended coroutine to be resumed after timeout
                submit_timer_task(h, m_d);
            }
            void await_resume() {}
        };

        return timer_awaitable{ d };
    }

    // also we can await other UpdateTask<T>
    auto await_transform(UpdateTask& update_task)
    {
        if (!update_task.handle)
        {
            throw std::runtime_error("coroutine without promise awaited");
        }

        if (update_task.handle.promise().awaiting_coroutine)
        {
            throw std::runtime_error("coroutine already awaited");
        }

        struct task_awaitable
        {
            std::coroutine_handle<UpdatePromise> handle;

            // check if this UpdateTask already has value computed
            bool await_ready()
            {
                return handle.done();
            }

            // h - is a handle to coroutine that calls co_await
            // store coroutine handle to be resumed after computing UpdateTask value
            void await_suspend(std::coroutine_handle<> h)
            {
                handle.promise().awaiting_coroutine = h;
            }

            // when ready return value to a consumer
            auto await_resume()
            {
            }
        };

        return task_awaitable{ update_task.handle };
    }
};

inline UpdateTask UpdatePromise::get_return_object()
{
    return { std::coroutine_handle<UpdatePromise>::from_promise(*this) };
}

// timer loop
void loop()
{
    while (!timers.empty())
    {
        auto& timer = timers.top();
        // if it is time to run a coroutine
        if (timer.target_time < std::chrono::steady_clock::now())
        {
            auto handle = timer.handle;
            timers.pop();
            handle.resume();
        }
        else
        {
            std::this_thread::sleep_until(timer.target_time);
        }
    }
}

// example

using namespace std::chrono_literals;

UpdateTask TestTimerAwait()
{
    using namespace std::chrono_literals;

    std::cout << "testTimerAwait started." << std::endl;

    co_await 1s;

    std::cout << "testTimerAwait finished." << std::endl;
}

UpdateTask TestNestedTimerAwait()
{
    using namespace std::chrono_literals;

    std::cout << "testNestedTimerAwait started." << std::endl;

    auto task = TestTimerAwait();

    co_await 2s;

    //We can't wait for a destroyed coroutine.
    //co_await task;

    std::cout << "testNestedTimerAwait finished." << std::endl;
}

// main can't be a coroutine and usually need some sort of looper (io_service or timer loop in this example)
int main()
{
    auto task = TestNestedTimerAwait();

    // execute deferred coroutines
    loop();
}

I compile the example with Microsoft (R) C/C++ Optimizing Compiler Version 19.30.30709 for x86 using the following command:

cl /std:c++latest /EHsc a.cpp

EDIT1:

The code was a bit incorrect, I commented co_await task out:

//We can't wait for a destroyed coroutine.
//co_await task;


Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source