task.hpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#pragma once


#include <felspar/coro/allocator.hpp>
#include <felspar/coro/coroutine.hpp>
#include <felspar/coro/forward.hpp>
#include <felspar/exceptions.hpp>

#include <optional>
#include <stdexcept>


namespace felspar::coro {


    template<typename Allocator>
    struct task_promise_base : private promise_allocator_impl<Allocator> {
        using promise_allocator_impl<Allocator>::operator new;
        using promise_allocator_impl<Allocator>::operator delete;

Flag to ensure the coroutine is started at appropriate points in time

22
        bool started = false;

Any caught exception that needs to be re-thrown is captured here

24
25
26
27
        std::exception_ptr eptr = {};
        void check_exception() {
            if (eptr) { std::rethrow_exception(eptr); }
        }

The continuation that is to run when the task is complete

29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
        coroutine_handle<> continuation = {};

        auto initial_suspend() const noexcept { return suspend_always{}; }
        void unhandled_exception() noexcept { eptr = std::current_exception(); }
        auto final_suspend() noexcept {
            return symmetric_continuation{std::exchange(continuation, {})};
        }
    };
    template<typename Allocator>
    struct task_promise<void, Allocator> final :
    public task_promise_base<Allocator> {
        using value_type = void;
        using allocator_type = Allocator;
        using handle_type = unique_handle<task_promise>;

        using task_promise_base<allocator_type>::eptr;
        using task_promise_base<allocator_type>::check_exception;

        task<void, allocator_type> get_return_object();

        bool has_returned = false;
        bool has_value() const noexcept { return has_returned or eptr; }
        void return_void() noexcept { has_returned = true; }

        void consume_value() {
            check_exception();
            if (not has_returned) {
                throw std::runtime_error{"The task hasn't completed"};
            }
        }
    };
    template<typename Y, typename Allocator>
    struct task_promise final : public task_promise_base<Allocator> {
        using value_type = Y;
        using allocator_type = Allocator;
        using handle_type = unique_handle<task_promise>;

        using task_promise_base<allocator_type>::eptr;
        using task_promise_base<allocator_type>::check_exception;

        task<value_type, allocator_type> get_return_object();

        std::optional<value_type> value = {};
        bool has_value() const noexcept { return value or eptr; }
        void return_value(value_type y) { value = std::move(y); }

        value_type consume_value() {
            check_exception();
            if (not value.has_value()) {
                throw std::runtime_error{
                        "The task hasn't completed with a value "};
            }
            value_type rv = std::move(*value);
            value.reset();
            return rv;
        }
    };

Tasks

 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
    template<typename Y, typename Allocator>
    class [[nodiscard]] task final {
        friend class eager<task>;
        friend class starter<task>;
        friend struct task_promise<Y, Allocator>;


      public:
        using value_type = Y;
        using allocator_type = Allocator;
        using promise_type = task_promise<value_type, allocator_type>;
        using handle_type = typename promise_type::handle_type;

Construction

Construct a new task from a previously released one

106
        explicit task(handle_type h) : coro{std::move(h)} {}

Not copyable

110
111
        task(task const &) = delete;
        task &operator=(task const &) = delete;

Movable

113
114
115
        task(task &&t) noexcept = default;
        task &operator=(task &&t) noexcept = default;
        ~task() = default;

Awaitable

119
120
        auto operator co_await() & = delete;
        auto operator co_await() && {

The awaitable takes over ownership of the coroutine handle once its been created. This ensures that the lifetime of the promise is long enough to deliver the return value.

126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
            struct awaitable {
                handle_type coro;

                bool await_ready() const noexcept {
                    return coro.promise().has_value();
                }
                coroutine_handle<>
                        await_suspend(coroutine_handle<> awaiting) noexcept {
                    if (not coro.promise().started) {
                        coro.promise().continuation = awaiting;
                        coro.promise().started = true;
                        return coro.get();
                    } else if (coro.promise().has_value()) {
                        return awaiting;
                    } else {
                        coro.promise().continuation = awaiting;
                        return noop_coroutine();
                    }
                }
                Y await_resume() { return coro.promise().consume_value(); }
            };
            return awaitable{std::move(coro)};
        }

Or use this from a normal function

152
153
154
155
156
157
        value_type get() & = delete;
        value_type get(
                source_location const &loc = source_location::current()) && {
            start(loc);
            return coro.promise().consume_value();
        }

Or take on responsibility for the coroutine

161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
        handle_type release() {
            auto c = std::move(coro);
            return c;
        }


      private:
        handle_type coro;

        void start(source_location const &loc = source_location::current()) {
            if (not coro) {
                throw stdexcept::runtime_error{
                        "Cannot start an empty task", loc};
            } else if (not coro.promise().started) {
                coro.promise().started = true;
                coro.resume();
            }
        }
    };


    template<typename Allocator>
    inline auto task_promise<void, Allocator>::get_return_object()
            -> task<void, allocator_type> {
        return task<void, allocator_type>{handle_type::from_promise(*this)};
    }
    template<typename T, typename Allocator>
    inline auto task_promise<T, Allocator>::get_return_object()
            -> task<value_type, allocator_type> {
        return task<value_type, allocator_type>{
                handle_type::from_promise(*this)};
    }


}