Search code examples
c++c++20c++-coroutine

Why coroutine_handle.done cannot return correct result?


I'm currently learning about coroutine in c++20. I encountered a problem. Here is my code:

//coroutine return object definition
class ReturnObject {
public:
    struct promise_type {
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { 
            return {}; 
        }
        ReturnObject get_return_object() { return ReturnObject(*this); }
        std::suspend_always yield_value(int val)
        {
            m_value = val;
            return std::suspend_always{};
        }
        void unhandled_exception() {}
        void return_void() {}
    private:
        int m_value;
    public:
        int GetValue() const { return m_value; }
    };
private:
    std::coroutine_handle<promise_type> m_handle;
public:
    decltype(m_handle) Handle() { return m_handle; }
    ReturnObject(promise_type& promise) : m_handle(std::coroutine_handle<promise_type>::from_promise(promise)){}
    int GetValue() const { return m_handle.promise().GetValue(); }
};

//range generator
ReturnObject Range(int startInc, int endInc, int step = 1)
{
    for (int i = startInc; i <= endInc; i += step)
    {
        co_yield i;
    }
}

//main function
int main()
{
    ReturnObject range = Range(1,10,3);
    while (!range.Handle().done())
    {
        std::cout << range.GetValue() << std::endl;
        range.Handle().resume();
    }
}

I found that the while-loop cannot quit correctly and it results in a runtime error. Could someone tell me why?


Solution

  • A coroutine_handle is like a raw pointer to the coroutine frame. In particular, like a raw pointer, the coroutine_handle does not have ownership and can become dangling if the coroutine frame goes away. That is what is happening here when Range() has completed its for-loop.

    The done() method is particularly misleading in this regard, as the name suggests that it is able to indicate whether the coroutine_handle is still valid. That is not the case! The same is true for the operator bool() on the coroutine_handle.

    The data that done() operates on is part of the coroutine frame and therefore when the handle becomes dangling, done() is also no longer functional. Instead done() only indicates whether the coroutine has reached its final suspension point, that is, it is currently suspended on the awaitable returned by final_suspend. If, as in your case, final_suspend does not trigger a suspension, the coroutine frame destroys itself and the coroutine_handle becomes dangling and must no longer be used.

    Then, you may ask, why does this design allow a coroutine to destroy itself in the first place? This is indeed a peculiar design choice. In most cases, it is just not useful and your rule of thumb should be to always suspend_always on the final_suspend. In very rare cases, you may end up with a design where the coroutine is supposed to destroy itself and where the surrounding system logic ensures that it will not be accessed anymore afterwards. But this is a highly specialized and rather advanced use case, so I would not worry about it at all when learning the feature.