-
-
Save Serikov/b28115e3b13a7c0ec45ab76468ddb0bd to your computer and use it in GitHub Desktop.
#include <coroutine> | |
#include <stdexcept> | |
#include <variant> | |
#include <memory> | |
namespace detail { | |
// simple type erasure for iterators | |
template<typename T> | |
struct generic_iterable | |
{ | |
virtual ~generic_iterable() = default; | |
virtual T& operator*() = 0; | |
virtual generic_iterable<T>& operator++() = 0; | |
virtual bool empty() const = 0; | |
bool await_ready() const noexcept | |
{ | |
return empty(); | |
} | |
template<typename U> | |
void await_suspend(std::coroutine_handle<U> h) noexcept | |
{ | |
h.promise().store_iterator(this); | |
} | |
void await_resume() const noexcept {} | |
}; | |
template<typename T, typename StartIterator, typename EndIterator> | |
struct iterator_iterable : public generic_iterable<T> | |
{ | |
iterator_iterable(StartIterator start, EndIterator end) : start(start), end(end) {} | |
T& operator*() override | |
{ | |
return *start; | |
} | |
iterator_iterable<T, StartIterator, EndIterator>& operator++() override | |
{ | |
++start; | |
return *this; | |
} | |
bool empty() const override | |
{ | |
return start == end; | |
} | |
StartIterator start; | |
EndIterator end; | |
}; | |
} // namespace detail | |
template<typename T> | |
struct generator | |
{ | |
using value_type = T; | |
struct promise_type | |
{ | |
// 0: prestart, 1: value, 2: range, 3: done | |
std::variant<std::monostate, T*, detail::generic_iterable<T>*, std::monostate> state; | |
promise_type* get_return_object() noexcept | |
{ | |
return this; | |
} | |
std::suspend_always initial_suspend() const noexcept | |
{ | |
return {}; | |
} | |
std::suspend_always final_suspend() const noexcept | |
{ | |
return {}; | |
} | |
std::suspend_always yield_value(T& value) noexcept | |
{ | |
state.template emplace<1>(std::addressof(value)); | |
return {}; | |
} | |
std::suspend_always yield_value(T&& value) noexcept | |
{ | |
state.template emplace<1>(std::addressof(value)); | |
return {}; | |
} | |
void return_void() noexcept | |
{ | |
state.template emplace<3>(); | |
} | |
template<typename Range> | |
auto await_transform(Range&& range) const noexcept | |
{ | |
using std::begin; | |
using std::end; | |
auto s = begin(range); | |
auto e = end(range); | |
// TODO: properly constraint | |
static_assert(std::is_same_v<decltype(*s), T&>); | |
detail::iterator_iterable<T, decltype(s), decltype(e)> iterator{s, e}; | |
return iterator; | |
} | |
void unhandled_exception() | |
{ | |
state.template emplace<3>(); | |
auto ex = std::current_exception(); | |
std::rethrow_exception(ex); | |
//// MSVC bug? should be possible to rethrow with "throw;" | |
//// rethrow exception immediately | |
// throw; | |
} | |
void store_iterator(detail::generic_iterable<T>* iterator) noexcept | |
{ | |
state.template emplace<2>(iterator); | |
} | |
T& value() | |
{ | |
switch (state.index()) { | |
case 1: | |
return *std::get<1>(state); | |
case 2: | |
return **std::get<2>(state); | |
case 0: | |
next(); | |
return value(); | |
default: | |
case 3: | |
throw std::logic_error("Generator already completed!"); | |
} | |
} | |
const T& value() const | |
{ | |
switch (state.index()) { | |
case 1: | |
return *std::get<1>(state); | |
case 2: | |
return **std::get<2>(state); | |
case 0: | |
next(); | |
return value(); | |
default: | |
case 3: | |
throw std::logic_error("Generator already completed!"); | |
} | |
} | |
void next() | |
{ | |
auto handle = std::coroutine_handle<promise_type>::from_promise(*this); | |
switch (state.index()) { | |
case 0: | |
case 1: | |
handle.resume(); | |
break; | |
case 2: { | |
auto& iterator = *std::get<2>(state); | |
++iterator; | |
if (iterator.empty()) { | |
state.template emplace<0>(); | |
handle.resume(); | |
} | |
break; | |
} | |
default: | |
case 3: | |
throw std::logic_error("Generator already completed!"); | |
} | |
} | |
}; | |
using handle_type = std::coroutine_handle<promise_type>; | |
struct iterator | |
{ | |
using iterator_category = std::input_iterator_tag; | |
using value_type = T; | |
using difference_type = ptrdiff_t; | |
using pointer = T*; | |
using reference = T&; | |
handle_type coro_handle; | |
iterator() : coro_handle(nullptr) {} | |
iterator(handle_type coro_handle) : coro_handle(coro_handle) {} | |
iterator& operator++() | |
{ | |
try { | |
coro_handle.promise().next(); | |
} catch (...) { | |
coro_handle = nullptr; | |
throw; | |
} | |
if (coro_handle.done()) | |
coro_handle = nullptr; | |
return *this; | |
} | |
iterator operator++(int) = delete; | |
bool operator==(iterator const& other) const | |
{ | |
return coro_handle == other.coro_handle; | |
} | |
bool operator!=(iterator const& other) const | |
{ | |
return !(*this == other); | |
} | |
const T& operator*() const | |
{ | |
return coro_handle.promise().value(); | |
} | |
const T* operator->() const | |
{ | |
return std::addressof(operator*()); | |
} | |
T& operator*() | |
{ | |
return coro_handle.promise().value(); | |
} | |
T* operator->() | |
{ | |
return std::addressof(operator*()); | |
} | |
}; | |
iterator begin() | |
{ | |
if (coro_handle) { | |
if (coro_handle.done()) | |
return {}; | |
} | |
return {coro_handle}; | |
} | |
iterator end() | |
{ | |
return {}; | |
} | |
generator(promise_type* promise) : coro_handle(handle_type::from_promise(*promise)) {} | |
generator() = default; | |
generator(generator const&) = delete; | |
generator& operator=(generator const&) = delete; | |
generator(generator&& other) : coro_handle(other.coro_handle) | |
{ | |
other.coro_handle = nullptr; | |
} | |
generator& operator=(generator&& other) | |
{ | |
if (&other != this) { | |
coro_handle = other.coro_handle; | |
other.coro_handle = nullptr; | |
} | |
return *this; | |
} | |
~generator() | |
{ | |
if (coro_handle) { | |
coro_handle.destroy(); | |
} | |
} | |
private: | |
std::coroutine_handle<promise_type> coro_handle = nullptr; | |
}; | |
template<typename T> | |
generator<int> range(T first, T last) | |
{ | |
while (first != last) { | |
co_yield first++; | |
} | |
} | |
template<typename T> | |
generator<int> range1(T first, T last) | |
{ | |
while (first != last) { | |
throw std::logic_error("BEGIN"); | |
co_yield first++; | |
} | |
} | |
template<typename T> | |
generator<int> range2(T first, T last) | |
{ | |
while (first != last) { | |
co_yield first++; | |
throw std::logic_error("ITERATOR"); | |
} | |
} | |
template<typename T> | |
generator<int> range4(T first, T last) | |
{ | |
co_return; | |
} | |
template<typename T> | |
generator<int> range5_ex(T first, T last) | |
{ | |
while (first != last) { | |
co_yield first++; | |
} | |
throw std::logic_error("AFTER LAST YIELD (for example cleanup failure)"); | |
} | |
///////////////////////// | |
generator<std::unique_ptr<int>> range_unqptr(int first, int last) | |
{ | |
while (first != last) { | |
co_yield std::make_unique<int>(first++); | |
} | |
} | |
generator<std::unique_ptr<int>> gen_refs() | |
{ | |
auto value = std::make_unique<int>(1); | |
co_yield value; // lvalue&, can be moved from | |
co_yield std::make_unique<int>(2); // rvalue&, can be moved from | |
const auto cvalue = std::make_unique<int>(3); | |
// co_yield cvalue; // compile error | |
} | |
generator<const std::unique_ptr<int>> gen_refs2() | |
{ | |
auto value = std::make_unique<int>(1); | |
co_yield value; // lvalue& | |
co_yield std::make_unique<int>(2); // rvalue& | |
const auto cvalue = std::make_unique<int>(3); | |
co_yield cvalue; // no compile error | |
} | |
///////////////////////// | |
// combining generators and ranges | |
generator<int> gen_and_then(generator<int> first, generator<int> second) | |
{ | |
co_await first; | |
co_yield 999; | |
co_await second; | |
co_yield - 999; | |
} | |
template<typename Range1, typename Range2> | |
generator<int> and_then(Range1 first, Range2 second) | |
{ | |
co_await first; | |
co_yield 999; | |
co_await second; | |
co_yield - 999; | |
} | |
#include <stdio.h> | |
#include <string> | |
#include <vector> | |
int main() | |
{ | |
// tests 1 | |
printf("\nrange\n"); | |
try { | |
for (int i : range(0, 10)) { | |
printf("%d\n", i); | |
} | |
} catch (std::exception const& e) { | |
printf("%s\n", e.what()); | |
} | |
printf("\nrange1\n"); | |
try { | |
for (int i : range1(0, 10)) { | |
printf("%d\n", i); | |
} | |
} catch (std::exception const& e) { | |
printf("%s\n", e.what()); | |
} | |
printf("\nrange2\n"); | |
try { | |
for (int i : range2(0, 10)) { | |
printf("%d\n", i); | |
} | |
} catch (std::exception const& e) { | |
printf("%s\n", e.what()); | |
} | |
try { | |
for (int i : range4(0, 10)) { | |
printf("%d\n", i); | |
} | |
} catch (std::exception const& e) { | |
printf("%s\n", e.what()); | |
} | |
printf("\nrange5_ex\n"); | |
try { | |
for (int i : range5_ex(0, 10)) { | |
printf("%d\n", i); | |
} | |
} catch (std::exception const& e) { | |
printf("%s\n", e.what()); | |
} | |
// tests 2 | |
try { | |
printf("\nrange_unqptr\n"); | |
for (auto& i : range_unqptr(0, 10)) { | |
auto b = std::move(i); // can be moved from, no UB | |
printf("%d\n", *b); | |
} | |
printf("\ngen_refs\n"); | |
for (auto& i : gen_refs()) { | |
auto b = std::move(i); // can be moved from, no UB | |
printf("%d\n", *b); | |
} | |
printf("\ngen_refs2\n"); | |
for (auto& i : gen_refs2()) { | |
// auto b = std::move(i); // compile time error | |
printf("%d\n", *i); | |
} | |
printf("\ngen_and_then\n"); | |
auto g1 = gen_and_then(range(0, 2), range(5, 10)); | |
for (auto i : g1) { | |
printf("%d\n", i); | |
} | |
printf("\ngen_and_then\n"); | |
auto g2 = gen_and_then(range(0, 2), range(5, 10)); | |
for (auto i : g2) { | |
printf("%d\n", i); | |
} | |
printf("\nand_then gen+gen\n"); | |
auto g3 = and_then(range(0, 2), range(5, 10)); | |
for (auto i : g3) { | |
printf("%d\n", i); | |
} | |
printf("\nand_then vec+vec\n"); | |
auto g4 = and_then(std::vector<int>{0, 1, 2}, std::vector<int>{5, 6, 7}); | |
for (auto i : g4) { | |
printf("%d\n", i); | |
} | |
printf("\nand_then vec+gen\n"); | |
auto g5 = and_then(std::vector<int>{0, 1, 2}, range(5, 10)); | |
for (auto i : g5) { | |
printf("%d\n", i); | |
} | |
// auto g6 = and_then(std::string("Hello world"), range(5, 10)); // compile error | |
auto lamda_gen = []() -> generator<const char> { | |
co_yield '\n'; | |
co_await "Hello"; | |
co_yield ' '; | |
co_await "generator!"; | |
co_yield '\n'; | |
}; | |
for (auto c : lamda_gen()) { | |
printf("%c", c); | |
} | |
} catch (std::exception const& e) { | |
printf("%s\n", e.what()); | |
} | |
try { | |
auto gen = range(5, 10); | |
for (auto i : gen) { | |
} | |
for (auto i : gen) { | |
printf("\nERROR: should be noop!\n"); | |
} | |
} catch (std::exception const& e) { | |
printf("Error: %s\n", e.what()); | |
} | |
// Calling begin multiple times | |
try { | |
auto gen = range(5, 8); | |
printf("\nShould print 5 6 7!\n"); | |
gen.begin(); | |
gen.begin(); | |
gen.begin(); | |
gen.begin(); | |
for (auto i : gen) { | |
printf("%d\n", i); | |
} | |
} catch (std::exception const& e) { | |
printf("Error: %s\n", e.what()); | |
} | |
} |
range | |
0 | |
1 | |
2 | |
3 | |
4 | |
5 | |
6 | |
7 | |
8 | |
9 | |
range1 | |
BEGIN | |
range2 | |
0 | |
ITERATOR | |
Generator already completed! | |
range5_ex | |
0 | |
1 | |
2 | |
3 | |
4 | |
5 | |
6 | |
7 | |
8 | |
9 | |
AFTER LAST YIELD (for example cleanup failure) | |
range_unqptr | |
0 | |
1 | |
2 | |
3 | |
4 | |
5 | |
6 | |
7 | |
8 | |
9 | |
gen_refs | |
1 | |
2 | |
gen_refs2 | |
1 | |
2 | |
3 | |
gen_and_then | |
0 | |
1 | |
999 | |
5 | |
6 | |
7 | |
8 | |
9 | |
-999 | |
gen_and_then | |
0 | |
1 | |
999 | |
5 | |
6 | |
7 | |
8 | |
9 | |
-999 | |
and_then gen+gen | |
0 | |
1 | |
999 | |
5 | |
6 | |
7 | |
8 | |
9 | |
-999 | |
and_then vec+vec | |
0 | |
1 | |
2 | |
999 | |
5 | |
6 | |
7 | |
-999 | |
and_then vec+gen | |
0 | |
1 | |
2 | |
999 | |
5 | |
6 | |
7 | |
8 | |
9 | |
-999 | |
Hello generator! | |
Should print 5 6 7! | |
5 | |
6 | |
7 |
There are very limited resources on the coroutines online could you recommend any so that i can have a deeper understanding of the same?
There were a couple of blog posts about coroutines but unfortunately I found nothing worth recommending.
Here is the list of materials that I used:
Lectures from Gor Nishanov:
CppCon 2014: Gor Nishanov "await 2.0: Stackless Resumable Functions"
CppCon 2015: Gor Nishanov “C++ Coroutines - a negative overhead abstraction"
First paper about corutines with Gor Nishanov as a co-author: Resumable Functions (revision 3)
Multiple revisions of Coroutine TS paper. It contains motivation, examples and description of the coroutine feature:
Working Draft, C++Extensions for Coroutines - 2017-07-30
Working Draft, C++Extensions for Coroutines - 2018-02-11
Working Draft, C++Extensions for Coroutines - 2018-06-24
Additions to coroutines with motivation and examples:
Add parameter preview to coroutine promise constructor
Add symmetric coroutine control transfer
Resulting C++ standard:
Working Draft, Standard for Programming Language C++
Thanks!!! Workable with latest MSVC and easy to learn.
Sadly it crashes when compiled with -std=c++20 both clang and gcc: https://godbolt.org/z/n3733bh1s
(And with -std=c++23 it does not compile)
@Maddimax Added workaround for GCC and Clang. Now compiles and runs on MSVC, Clang and GCC with C++20 and C++23 (GCC and Clang): https://godbolt.org/z/1nGMonna3
As stated in the standard "The expression promise.get_return_object() is used to initialize the returned reference or prvalue result object of a call to a coroutine"](https://eel.is/c++draft/dcl.fct.def.coroutine#7). So I think that both GCC and Clang are wrong here as error text suggests that both of them were trying to initialize generator
with rvalue of a promise
instead of promise&
which is the actual result of promise.get_return_object()
expression:
Clang:
error: cannot bind non-const lvalue reference of type 'generator<std::unique_ptr<int> >::promise_type&' to an rvalue of type 'generator<std::unique_ptr<int> >::promise_type'
GCC:
error: no viable conversion from returned value of type 'promise_type' to function return type 'generator<std::unique_ptr<int>>'
338 | generator<std::unique_ptr<int>> range_unqptr(int first, int last)
| ^~~~~~~~~~~~
<source>:263:5: note: candidate constructor not viable: expects an lvalue for 1st argument
There are very limited resources on the coroutines online could you recommend any so that i can have a deeper understanding of the same?