Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ coroutines: implementing task<void>

So, there I'm trying to comprehend that new & complex concept of the coroutines. Took Clang for it, compiling via clang++ -std=c++17 -fcoroutines-ts -stdlib=libc++ goes fine.

One of the most useful concepts is task<> coroutine type, it's mentioned here and even has a couple interesting implementations, by Gor Nishanov and in cppcoro library.

Okay, looked fine to try myself in the simpliest case. So, the goal is to implement something that should work like the following:

    {
        auto producer = []() -> task<int> {
            co_return 1;
        };

        auto t = producer();

        assert(!t.await_ready());
        assert(t.result() == 1);
        assert(t.await_ready());
    }

The template class task<> itself was made quite straightforward:

#pragma once

#include <experimental/coroutine>
#include <optional>

namespace stdx = std::experimental;

template <typename T=void>
struct task 
{
    template<typename U>
    struct task_promise;

    using promise_type = task_promise<T>;
    using handle_type = stdx::coroutine_handle<promise_type>;

    mutable handle_type m_handle;

    task(handle_type handle)
        : m_handle(handle) 
    {}

    task(task&& other) noexcept 
        : m_handle(other.m_handle)
    { other.m_handle = nullptr; };

    bool await_ready() 
    { return m_handle.done(); }

    bool await_suspend(stdx::coroutine_handle<> handle) 
    {
        if (!m_handle.done()) {
            m_handle.resume();
        }

        return false;
    }

    auto await_resume() 
    { return result(); }

    T result() const 
    {     
        if (!m_handle.done())
            m_handle.resume();  

        if (m_handle.promise().m_exception)
            std::rethrow_exception(m_handle.promise().m_exception);

        return *m_handle.promise().m_value;
    }

    ~task() 
    {
        if (m_handle)
            m_handle.destroy();
    }

    template<typename U>
    struct task_promise 
    {
        std::optional<T>    m_value {};
        std::exception_ptr  m_exception = nullptr;

        auto initial_suspend() 
        { return stdx::suspend_always{}; }

        auto final_suspend() 
        { return stdx::suspend_always{}; }

        auto return_value(T t) 
        {
            m_value = t;
            return stdx::suspend_always{};
        }

        task<T> get_return_object()
        { return {handle_type::from_promise(*this)}; }

        void unhandled_exception() 
        {  m_exception = std::current_exception(); }

        void rethrow_if_unhandled_exception()
        {
            if (m_exception)
                std::rethrow_exception(std::move(m_exception));
        }
    };

};

Couldn't really make a smaller piece of code complete and compilable, sorry. Anyway it worked somehow, but there still remained the case of task<void>, it's usage could be like the following:

    {
        int result = 0;

        auto int_producer = []() -> task<int> {
            co_return 1;
        };

        auto awaiter = [&]() -> task<> { // here problems begin
            auto i1 = co_await int_producer();
            auto i2 = co_await int_producer();

            result = i1 + i2;
        };

        auto t = awaiter();

        assert(!t.await_ready());
        t.await_resume();
        assert(result == 2);
    }

The latter didn't seem a problem at all, it looked like task_promise<U> required a specialization for void (could be a non-template struct without that void case). So, I tried it:

    template<>
    struct task_promise<void>
    {
        std::exception_ptr  m_exception;

        void return_void() noexcept  {}

        task<void> get_return_object() noexcept
        { return {handle_type::from_promise(*this)}; }

        void unhandled_exception() 
        { m_exception = std::current_exception(); }

        auto initial_suspend() 
        { return stdx::suspend_always{}; }

        auto final_suspend() 
        { return stdx::suspend_always{}; }
    };

Neat and simple... and it causes segfault without any readable stacktrace =( Works fine when task<> is changed to any non-void template, like task<char>.

What is wrong with my template specialization? Or am I missing some tricky concept with those coroutines?

Would be grateful for any ideas.

like image 756
MasterAler Avatar asked May 13 '19 05:05

MasterAler


1 Answers

Apparently, the usual suspect was the criminal: specialization! From the standard itself [temp.expl.spec]/7

When writing a specialization, be careful about its location; or to make it compile will be such a trial as to kindle its self-immolation.

To avoid issue, let's make it as simple as possible: task_promise can be a non template and the member specialization is declared as soon as possible:

template<class T=void>
struct task{
  //...
  struct task_promise{
    //...
    };
  };

//member specialization declared before instantiation of task<void>;
template<>
struct task<void>::task_promise{
  //...
  };
like image 85
Oliv Avatar answered Oct 25 '22 06:10

Oliv