Сопрограммы C ++: реализация задачи <void> - PullRequest
6 голосов
/ 13 мая 2019

Итак, я пытаюсь понять эту новую и сложную концепцию сопрограмм. Взял Clang для этого, компиляция через clang++ -std=c++17 -fcoroutines-ts -stdlib=libc++ идет нормально.

Одним из наиболее полезных понятий является task<> тип сопрограммы, он упоминается здесь и даже имеет пару интересных реализаций, от Gor Nishanov и в библиотеке cppcoro .

Ладно, выглядел хорошо, чтобы попробовать себя в простейшем случае. Итак, цель состоит в том, чтобы реализовать что-то, что должно работать следующим образом:

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

        auto t = producer();

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

Сам шаблон класса task<> был сделан довольно просто:

#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));
        }
    };

};

Не могу сделать небольшой фрагмент кода полным и компилируемым, извините. В любом случае это работало как-то, но все еще оставался случай task<void>, его использование могло бы быть следующим:

    {
        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);
    }

Последнее совсем не казалось проблемой, похоже, что task_promise<U> требует специализации для void (это может быть не шаблонная структура без этого пустого регистра). Итак, я попробовал это:

    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{}; }
    };

Аккуратный и простой ... и он вызывает segfault без читаемой трассировки стека = ( Работает нормально, когда task<> заменен на любой не пустой шаблон, например task<char>.

Что не так с моей специализацией по шаблонам? Или мне не хватает какой-то хитрой концепции с этими сопрограммами?

Буду благодарен за любые идеи.

1 Ответ

2 голосов
/ 13 мая 2019

Видимо, обычный подозреваемый был преступником: специализация! Из самого стандарта [temp.expl.spec] / 7

При написании специализации будьте осторожны с ее местоположением; или сделать его компиляцией будет таким испытанием, чтобы разжечь его самосожжение.

Чтобы избежать проблем, давайте сделаем это как можно проще: task_promise может быть не шаблонным, а специализация члена объявляется как можно скорее:

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

//member specialization declared before instantiation of task<void>;
template<>
struct task<void>::task_promise{
  //...
  };
...