April 20th, 2021

C++ coroutines: Getting rid of our atomic variant discriminator

We continue the refinement of our coroutine implementation by removing the atomic variable used as the discriminant of our result holder variant.

The discriminant needed to be atomic because we used it in await_ready to peek at whether the coroutine had completed. But we’ve switched over to using the atomic m_waiting member to track the coroutine state, which means that the use of the discriminant is now protected by the memory ordering requirements of m_waiting. The discriminant itself can now be a regular variable.

    template<typename T>
    struct simple_promise_holder
    {
        ...
        // std::atomic<result_status>
        result_status status
            { result_status::empty };

        template<typename...Args>
        void set_value(Args&&... args)
        {
            new (std::addressof(result.wrap))
                wrapper<T>{ std::forward<Args>(args)... };
            status = result_status::value;
        }

        void unhandled_exception() noexcept
        {
            new (std::addressof(result.error))
                std::exception_ptr(std::current_exception());
            status = result_status::error;
        }

        // bool is_empty() const noexcept
        // {
        //     return status.load(std::memory_order_relaxed) ==
        //         result_status::empty;
        // }

        T get_value()
        {
            switch (status) {
            case result_status::value:
                return result.wrap.get_value();
            case result_status::error:
                std::rethrow_exception(
                    std::exchange(result.error, {}));
            }
            assert(false);
            std::terminate();
        }

        ~simple_promise_result_holder()
        {
            switch (status) {
            case result_status::value:
                result.wrap.~wrapper();
                break;
            case result_status::error:
                if (result.error)
                    std::rethrow_exception(result.error);
                result.error.~exception_ptr();
            }
        }
    };

What used to be status.store is now just an assignment, and what used to be status.load now just a read.

We can also get rid of the is_empty method, since it was used only by our previous version of client_await_ready, which we abandoned when we switched to using m_waiting.

Next time, we’ll add support for cold-start coroutines.

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

1 comment

Discussion is closed. Login to edit/delete existing comments.

  • Taras .

    Is it okay to throw from the destructor of `simple_promise_result_holder`?
    If it is in this particular case, then probably this destructor should be marked as `noexcept(false)`, since destructors are `noexcept` by default.