How to recursively yield generator by overloading yield_value? - c++

I've created a generator that will have an overload operator* in order to be converted into std::ranges::subrange and I also want to overload yield_value from promise_type that accepts a subrange type that will be yielded recursively.
Source Code:
template <typename T>
class [[nodiscard]] generator {
public:
using value_type = T;
struct promise_type;
using handle_type = std::coroutine_handle<promise_type>;
private:
handle_type handle_ { nullptr };
explicit generator(handle_type handle) : handle_(handle) {}
public:
struct promise_type {
value_type value_;
generator<value_type> get_return_object() {
return generator{ handle_type::from_promise(*this) };
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() { return {}; }
void unhandled_exception() { std::terminate(); }
std::suspend_always yield_value(const value_type& value) noexcept {
value_ = value;
return {};
}
template <typename U>
std::suspend_never await_transform(U&&) = delete;
void return_void() {}
};
generator() noexcept = default;
generator(const generator&) = delete;
generator(generator&& other) noexcept
: handle_(std::move(other.handle_)) {
other.handle_ = nullptr;
}
~generator() { if (handle_) handle_.destroy(); }
generator& operator=(const generator&) = delete;
generator& operator=(generator&& other) noexcept {
handle_ = std::move(other.handle_);
other.handle_ = nullptr;
return *this;
}
void swap(generator& other) noexcept {
using std::swap;
swap(handle_, other.handle_);
}
class iterator {
private:
handle_type handle_;
friend generator;
explicit iterator(handle_type handle) noexcept
: handle_(handle) {}
public:
using value_type = std::remove_cvref_t<T>;
using reference = value_type&;
using const_reference = const value_type&;
using pointer = value_type*;
using const_pointer = const value_type*;
using size_type = std::size_t;
using difference_type = std::ptrdiff_t;
using iterator_category = std::input_iterator_tag;
iterator() noexcept = default;
friend bool operator==(const iterator& iter, std::default_sentinel_t) noexcept {
return iter.handle_.done();
}
friend bool operator==(std::default_sentinel_t s, const iterator& iter) noexcept {
return (iter == s);
}
iterator& operator++() {
if (handle_.done()) handle_.promise().unhandled_exception();
handle_.resume();
return *this;
}
iterator operator++(int) {
auto temp = *this;
++*this;
return temp;
}
reference operator*() noexcept {
return handle_.promise().value_;
}
pointer operator->() noexcept {
return std::addressof(operator*());
}
};
iterator begin() noexcept {
if (handle_) {
handle_.resume();
if (handle_.done())
handle_.promise().unhandled_exception();
}
return iterator{handle_};
}
std::default_sentinel_t end() noexcept {
return std::default_sentinel;
}
};
Example:
auto generate_0(int n) -> generator<int> {
while (n != 0)
co_yield n--;
}
auto generate_1() -> generator<int> {
for (const auto& elem : generate_0(10)) {
co_yield elem;
}
}
generate_1 will work obviously but I want have the same output like the generate_1 that each element is co_yield-ed directly inside the yield_value:
auto generate_1() -> generator<int> {
co_yield* generate_0(10);
}
Such that:
In class generator:
auto operator*() {
return std::ranges::subrange(begin(), end());
}
In nested class generator<...>::promise_type:
template <typename U>
std::suspend_always yield_value(const std::ranges::subrange<U, std::default_sentinel_t>& r) noexcept {
/** ... **/
return {};
}

First things first: bugs/odd bits on your end.
I don't think it's worth it trying to support old-style iterators. It doesn't make sense to default-construct generator<T>::iterator, and the new-style iterator concepts do not require it. You can tear out a lot of junk from iterator.
Also, == is magical. If x == y doesn't find a matching operator== but y == x does, then x == y is automatically rewritten to y == x. You don't need to provide both operator==s.
The promise_type does not need to hold T by value. An odd thing about yielding things from coroutines is that if you make yield_value take by-reference, you can get a reference to something that lives in the coroutine state. But the coroutine state is preserved until you resume it! So promise_type can instead hold T const*. Now you no longer require annoying things like copyability and default-constructibility from T.
It appears to be unnatural for a generator to initially suspend. Currently, if you do g.begin(); g.begin();, you will advance the generator even though you've incremented no iterator. If you make g.begin() not resume the coroutine and remove the initital suspension, everything just works. Alternatively, you could make generator track whether it has started the coroutine and only advance it to the first yield on begin(), but that's complicated.
While calling std::terminate() on every operation that's normally UB may be nice, it's also noisy and I'm just not going to include it in this answer. Also, please don't call it via unhandled_exception. That's just confusing: unhandled_exception has one very specific purpose and meaning and you are just not respecting that.
generator<T>::operator=(generator&&) leaks *this's coroutine state! Also, your swap is nonstandard because it is not a free 2-arg function. We can fix these by making operator= do what swap did and then getting rid of swap because std::swap works.
From a design/theory standpoint, I think it makes more sense to implement this syntax instead.
auto generate_1() -> generator<int> {
co_await generate_0(10);
}
A generator can temporarily give up control to another and may resume running after it awaits for the inner generator to run out. Implementing something to yield from a range can be easily implemented atop this by making a generator wrapping the range. This also lines up with the syntax in other languages like Haskell.
Now, coroutines have no stack. That means that as soon as we cross a function call boundary away from a coroutine like generate_1, it is not possible to suspend/resume that function via the coroutine state associated with the caller. So we have to implement our own stack, where we extend our coroutine state (promise_type) with the ability to record that it is currently pulling from another coroutine instead of having its own value. (Please note this would also apply to yielding from a range: whatever function is called to receive the range from generator_1 will not be able to control generator_1's coroutine.) We do this by making promise_type hold a
std::variant<T const*, std::subrange<iterator, std::default_sentinel_t>> value;
Note that promise_type does not own the generator represented by the subrange. Most of the time (as it is in generator_1) the same trick as yield_value applies: the generator which owns the sub-coroutine's state lives inside the caller coroutine's stack.
(This is also a point against directly implementing co_yield from a range: we need to fix the type of whatever is going into promise_type. From an API standpoint, it's understandable for co_await inside a generator<T> to accept generator<T>s. But if we implemented co_yield we'd only be able to directly handle one specific kind of rangeā€”a subrange wrapping a generator. That'd be weird. And to do otherwise we'd need to implement type-erasure; but the most obvious way to type-erase a range in this context is to make a generator. So we're back to a generator awaiting on another as being the more fundamental operation.)
The stack of running generators is now a linked-list threaded through their promise_types. Everything else just writes itself.
struct suspend_maybe { // just a general-purpose helper
bool ready;
explicit suspend_maybe(bool ready) : ready(ready) { }
bool await_ready() const noexcept { return ready; }
void await_suspend(std::coroutine_handle<>) const noexcept { }
void await_resume() const noexcept { }
};
template<typename T>
class [[nodiscard]] generator {
public:
struct iterator;
struct promise_type;
using handle_type = std::coroutine_handle<promise_type>;
using range_type = std::ranges::subrange<iterator, std::default_sentinel_t>;
private:
handle_type handle;
explicit generator(handle_type handle) : handle(std::move(handle)) { }
public:
class iterator {
private:
handle_type handle;
friend generator;
explicit iterator(handle_type handle) noexcept : handle(handle) { }
public:
// less clutter
using iterator_concept = std::input_iterator_tag;
using value_type = std::remove_cvref_t<T>;
using difference_type = std::ptrdiff_t;
// just need the one
bool operator==(std::default_sentinel_t) const noexcept {
return handle.done();
}
// need to muck around inside promise_type for this, so the definition is pulled out to break the cycle
inline iterator &operator++();
void operator++(int) { operator++(); }
// again, need to see into promise_type
inline T const *operator->() const noexcept;
T const &operator*() const noexcept {
return *operator->();
}
};
iterator begin() noexcept {
return iterator{handle};
}
std::default_sentinel_t end() const noexcept {
return std::default_sentinel;
}
struct promise_type {
// invariant: whenever the coroutine is non-finally suspended, this is nonempty
// either the T const* is nonnull or the range_type is nonempty
// note that neither of these own the data (T object or generator)
// the coroutine's suspended state is often the actual owner
std::variant<T const*, range_type> value = nullptr;
generator get_return_object() {
return generator(handle_type::from_promise(*this));
}
// initially suspending does not play nice with the conventional asymmetry between begin() and end()
std::suspend_never initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { std::terminate(); }
std::suspend_always yield_value(T const &x) noexcept {
value = std::addressof(x);
return {};
}
suspend_maybe await_transform(generator &&source) noexcept {
range_type range(source);
value = range;
return suspend_maybe(range.empty());
}
void return_void() { }
};
generator(generator const&) = delete;
generator(generator &&other) noexcept : handle(std::move(other.handle)) {
other.handle = nullptr;
}
~generator() { if(handle) handle.destroy(); }
generator& operator=(generator const&) = delete;
generator& operator=(generator &&other) noexcept {
// idiom: implementing assignment by swapping means the impending destruction/reuse of other implicitly handles cleanup of the resource being thrown away (which originated in *this)
std::swap(handle, other.handle);
return *this;
}
};
// these are both recursive because I can't be bothered otherwise
// feel free to change that if it actually bites
template<typename T>
inline auto generator<T>::iterator::operator++() -> iterator& {
struct visitor {
handle_type handle;
void operator()(T const*) { handle(); }
void operator()(range_type &r) {
if(r.advance(1).empty()) handle();
}
};
std::visit(visitor(handle), handle.promise().value);
return *this;
}
template<typename T>
inline auto generator<T>::iterator::operator->() const noexcept -> T const* {
struct visitor {
T const *operator()(T const *x) { return x; }
T const *operator()(range_type &r) {
return r.begin().operator->();
}
};
return std::visit(visitor(), handle.promise().value);
}
Nothing appears to be on fire.
static_assert(std::ranges::input_range<generator<unsigned>>); // you really don't need all that junk in iterator!
generator<unsigned> generate_0(unsigned n) {
while(n != 0) co_yield n--;
}
generator<unsigned> generate_1(unsigned n) {
co_yield 0;
co_await generate_0(n);
co_yield 0;
}
int main() {
auto g = generate_1(5);
for(auto i : g) std::cout << i << "\n"; // 0 5 4 3 2 1 0 as expected
// even better, asan is happy!
}
If you want to yield values from an arbitrary range, I would just implement this type-eraser.
auto generate_all(std::ranges::input_range auto &&r) -> generator<std::ranges::range_value_t<decltype(r)>> {
for(auto &&x : std::forward<decltype(r)>(r)) co_yield std::forward<decltype(x)>(x);
}
So you get e.g.
generator<unsigned> generate_1(unsigned n) {
co_await generate_all(std::array{41u, 42u, 43u});
co_await generate_0(n);
co_yield 0;
}

Related

How would you implement a lazy "range factory" for C++20 ranges that just calls a generator function?

I like the idea of the lazy ranges you can make with std::views::iota but was surprised to see that iota is currently the only thing like it in the standard; it is the only "range factory" besides views::single and views::empty. There is not currently, for example, the equivalent of std::generate as a range factory.
I note however it is trivial to implement the semantics of generate by using a transform view on iota and just ignoring the value iota passes to transform i.e.
#include <iostream>
#include <ranges>
#include <random>
template<typename F>
auto generate1(const F& func) {
return std::views::iota(0) | std::views::transform([&func](int) {return func(); });
}
std::random_device dev;
std::mt19937 rng(dev());
int main() {
auto d6 = []() {
static std::uniform_int_distribution<> dist(1, 6);
return dist(rng);
};
for (int v : generate1(d6) | std::views::take(10)) {
std::cout << v << ' ';
}
std::cout << '\n';
}
My questions is what would be "the real way" to implement something like this? To make a range view object that is pipeable that does not just use iota.
I tried inheriting from ranges::view_interface -- no idea if this is the correct approach -- and just having it return a dummy iterator that calls a generator function but my code doesn't work because of the part where it needs to pipe the range view to std::views::take in order to not cause an infinite loop. The object I define here does not end up being pipeable.
#include <iostream>
#include <ranges>
#include <random>
template<typename F>
class generate2 : public std::ranges::view_interface<generate2<F>>
{
using value_type = decltype(std::declval<F>()());
class iterator {
const F* gen_func_;
public:
iterator(const F* f) : gen_func_(f)
{}
value_type operator*() const {
return (*gen_func_)();
}
bool operator!=(const iterator&) {
return true;
}
iterator& operator++() {
return *this;
}
};
F generator_func_;
public:
generate2(const F& f) : generator_func_(f) {
}
iterator begin() {
return iterator(&generator_func_);
}
iterator end() {
return iterator(nullptr);
}
};
std::random_device dev;
std::mt19937 rng(dev());
int main() {
auto d6 = []() {
static std::uniform_int_distribution<> dist(1, 6);
return dist(rng);
};
// the following doesnt compile because of the pipe...
for (int v : generate2(d6) | std::views::take(10)) {
std::cout << v << ' ';
}
std::cout << '\n';
}
The reason why generate2 cannot work is that it does not model the range concept, that is, the type returned by its begin() does not model input_iterator, because input_iterator requires difference_type and value_type to exist and i++ is a valid expression.
In addition, your iterator does not satisfy sentinel_for<iterator>, which means that it cannot serve as its own sentinel, because sentinel_for requires semiregular which requires default_initializable, so you also need to add default constructors for it.
You also need to rewrite bool operator!=(...) to bool operator==(...) const since operator!= does not reverse synthesize operator==. But it's easier to just use default_sentinel_t as sentinel in your case.
if you add them to iterator you will find the code will be well-formed:
class iterator {
public:
using value_type = decltype(std::declval<F>()());
using difference_type = std::ptrdiff_t;
iterator() = default;
void operator++(int);
bool operator==(const iterator&) const {
return false;
}
// ...
};
However, the operator*() of iterator does not meet the requirements of equality-preserving, that is to say, the results obtained by the two calls before and after are not equal, which means that this will be undefined behavior.
You can refer to the implementation of ranges::istream_view to use a member variable to cache each generated result, then you only need to return the cached value each time iterator::operator*() is called.
template<typename F>
class generate2 : public std::ranges::view_interface<generate2<F>> {
public:
auto begin() {
value_ = generator_func_();
return iterator{*this};
}
std::default_sentinel_t end() const noexcept { return std::default_sentinel; }
class iterator {
public:
//...
value_type operator*() const {
return parent_->value_;
}
private:
generate2* parent_;
};
private:
F generator_func_;
std::remove_cvref_t<std::invoke_result_t<F&>> value_;
};

Create contiguous_iterator for custom class

Summary
I have a custom array class:
template<typename T, int SIZE>
class Array {
private:
T mArray[SIZE];
};
To enable support for std algorithms, with ranges, I want to create an iterator for this class. It would seem that std::contiguous_iterator would be the optimal choice since I can guarantee contiguous memory layout for the data. Following the iterator tutorial I should create a class inside this class. However, I should somehow be (quoted) "For example, instead of the std::forward_iterator_tag tag you would mark your iterator with the std::forward_iterator concept.".
I have a hard time figuring out what the syntax would look like for this, and I have been unable to find a post on the web showcasing this.
Question
How do I complete the following code snippet to implement std::contiguous_iterator for my Array<T,S> class?:
import <iterator>;
template<typename T, int SIZE>
class Array {
public:
const T& operator[](int i) { return mArray[i]; }
T& operator[](int i) { return mArray[i]; }
private:
T mArray[SIZE];
public:
struct Iterator {
Iterator(T* ptr) : mPtr(ptr) {}
private:
T* mPtr;
};
Iterator begin() { return Iterator(&mArray[0]); }
Iterator end() { return Iterator(&mArray[SIZE]); }
};
NOTE: There is a lot of operator overloads. An answer is not required to provide all of them. I just need an example syntax for where to place the concept, then I can probably figure out the rest.
As far as I could tell you need typedefs on the iterator class, so simply using a pointer was not sufficient. Here is an example:
#include <iterator>
#include <algorithm>
template<typename T, int SIZE>
class Array {
public:
const T& operator[](int i) const { return mArray[i]; }
T& operator[](int i) { return mArray[i]; }
private:
T mArray[SIZE];
public:
struct iterator
{
using difference_type=std::ptrdiff_t;
using value_type=std::remove_cv_t<T>;
using pointer=T*;
using reference=T&;
using iterator_category=std::random_access_iterator_tag;
using iterator_concept=std::contiguous_iterator_tag;
using self_type=iterator;
iterator(T *x) : ptr(x) {}
T operator*() { return *ptr; }
T operator->() { return ptr; }
difference_type operator-(const iterator& rhs) { return ptr-rhs.ptr; }
iterator& operator ++() { ++ptr; return *this;}
bool operator !=(const iterator& rhs) { return ptr != rhs.ptr; }
private:
T * ptr;
};
iterator begin() { return &mArray[0]; }
iterator end() { return &mArray[SIZE]; }
};
int foo(Array<int, 7>& a)
{
int sum;
for (auto x : a)
{
sum+=x;
}
return sum;
}
int goo(Array<int, 7>& a, int x)
{
auto ret=std::find(a.begin(), a.end(), x);
if (ret!=a.end()) return *ret;
return 0;
}
Note that you would likely need const_iterator and reverse_iterators for const and non-const ...
Credits
Thanks to #glenn-teitelbaum for pointing me in the right direction. I think I managed to figure out how to do this. It took a long time, so this will hopefully save someone else that trouble.
[Answering my own question]
The iterator should comply with the std::contiguous_iterator concept, so I looked at cppreference for the necessary parts. The concept is defined like this:
template<class I>
concept contiguous_iterator =
std::random_access_iterator<I> &&
std::derived_from</*ITER_CONCEPT*/<I>, std::contiguous_iterator_tag> &&
std::is_lvalue_reference_v<std::iter_reference_t<I>> &&
std::same_as<
std::iter_value_t<I>, std::remove_cvref_t<std::iter_reference_t<I>>
> &&
requires(const I& i) {
{ std::to_address(i) } ->
std::same_as<std::add_pointer_t<std::iter_reference_t<I>>>;
};
So in order to implement this concept, I must first also implement std::random_access_iterator, std::is_derived_from<[...]>, std::is_lvalue_reference_v<[...]>, and so on. This is a recursive process, especially since std::random_access_iterator builds on top of 4 other iterator types. Since this is a C++20 question, I aim to use C++20 features as much as possible (since it greatly simplifies the implementation). This is the complete iterator implementation:
template<typename T, int SIZE>
class Array
{
public:
class Iterator
{
public:
using iterator_category = std::contiguous_iterator_tag;
using iterator_concept = std::contiguous_iterator_tag;
//using difference_type = std::ptrdiff_t; // Likely the same
using difference_type = typename std::iterator<
std::contiguous_iterator_tag, T>::difference_type;
//using value_type = T;
using value_type = std::remove_cv_t<T>; // Using `T` seems sufficient
using pointer = T*;
using reference = T&;
// constructor for Array<T,S>::begin() and Array<T,S>::end()
Iterator(pointer ptr) : mPtr(ptr) {}
// std::weakly_incrementable<I>
Iterator& operator++() { ++mPtr; return *this; }
Iterator operator++(int) { Iterator tmp = *this; ++(*this); return tmp; }
Iterator() : mPtr(nullptr/*&mArray[0]*/) {} // TODO: Unsure which is correct!
// std::input_or_output_iterator<I>
reference operator*() { return *mPtr; }
// std::indirectly_readable<I>
friend reference operator*(const Iterator& it) { return *(it.mPtr); }
// std::input_iterator<I>
// No actions were needed here!
// std::forward_iterator<I>
// In C++20, 'operator==' implies 'operator!='
bool operator==(const Iterator& it) const { return mPtr == it.mPtr; }
// std::bidirectional_iterator<I>
Iterator& operator--() { --mPtr; return *this; }
Iterator operator--(int) { Iterator tmp = *this; --(*this); return tmp; }
// std::random_access_iterator<I>
// std::totally_ordered<I>
std::weak_ordering operator<=>(const Iterator& it) const {
return std::compare_three_way{}(mPtr, it.mPtr);
// alternatively: `return mPtr <=> it.mPtr;`
}
// std::sized_sentinel_for<I, I>
difference_type operator-(const Iterator& it) const { return mPtr - it.mPtr; }
// std::iter_difference<I> operators
Iterator& operator+=(difference_type diff) { mPtr += diff; return *this; }
Iterator& operator-=(difference_type diff) { mPtr -= diff; return *this; }
Iterator operator+(difference_type diff) const { return Iterator(mPtr + diff); }
Iterator operator-(difference_type diff) const { return Iterator(mPtr - diff); }
friend Iterator operator+(difference_type diff, const Iterator& it) {
return it + diff;
}
friend Iterator operator-(difference_type diff, const Iterator& it) {
return it - diff;
}
reference operator[](difference_type diff) const { return mPtr[diff]; }
// std::contiguous_iterator<I>
pointer operator->() const { return mPtr; }
using element_type = T;
private:
T* mPtr;
};
// === STATIC ASSERTS ===
// - to verify correct Iterator implementation!
static_assert(std::weakly_incrementable<Iterator>);
static_assert(std::input_or_output_iterator<Iterator>);
static_assert(std::indirectly_readable<Iterator>);
static_assert(std::input_iterator<Iterator>);
static_assert(std::incrementable<Iterator>);
static_assert(std::forward_iterator<Iterator>);
static_assert(std::bidirectional_iterator<Iterator>);
static_assert(std::totally_ordered<Iterator>);
static_assert(std::sized_sentinel_for<Iterator, Iterator>);
static_assert(std::random_access_iterator<Iterator>);
static_assert(std::is_lvalue_reference_v<std::iter_reference_t<Iterator>>);
static_assert(std::same_as<std::iter_value_t<Iterator>,
std::remove_cvref_t<std::iter_reference_t<Iterator>>>);
static_assert(std::contiguous_iterator<Iterator>);
const T& operator[](int i) const {
if (i < 0 || i >= SIZE) {
throw std::runtime_error("Array index out of bounds");
}
return mArray[i];
}
T& operator[](int i) {
if (i < 0 || i >= SIZE) {
throw std::runtime_error("Array index out of bounds");
}
return mArray[i];
}
Iterator begin() { return Iterator(&mArray[0]); }
Iterator end() { return Iterator(&mArray[SIZE]); }
private:
T mArray[SIZE];
};
// Check that the Array class can be used as a contiguous_range.
static_assert(std::ranges::contiguous_range<Array<int, 10>>);
NOTE: using element_type = T; was necessary because of a bug in the specification, which might be fixed. I found information about that here. Adding this fixed issue with std::to_address<Iterator> not being able to compile, and was the last missing piece in going from std::random_access_iterator to std::contiguous_iterator.
Testing
I did not perform a complete testing suite with all algorithms, but I chose a few which depend on ranges and std::random_access_iterator. It all runs smoothly. I also depend on building standard library headers as module units, because I want to showcase how C++20 features work together.
import <stdexcept>;
import <iostream>;
import <iterator>;
import <algorithm>;
import <random>;
#include <memory> // fails to build header unit!
template<typename T, int SIZE>
class Array
{
[...]
};
int main()
{
Array<int, 10> arr;
for (int i = 0; i < 10; i++) arr[i] = i;
// I need to call std::ragnes::shuffle since that depends on
// std::random_access_iterator, so that is a minimum.
// https://en.cppreference.com/w/cpp/algorithm/ranges/shuffle
std::random_device rd;
std::mt19937 gen{rd()};
std::cout << "before random shuffle:\n";
for (auto& i : arr) std::cout << i << ' ';
std::ranges::shuffle(arr, gen);
std::cout << "\nafter random shuffle:\n";
for (auto& i : arr) std::cout << i << ' ';
std::cout << '\n';
// Also std::ranges::stable_sort is a good check (also random_access_iterator):
// https://en.cppreference.com/w/cpp/algorithm/ranges/stable_sort
std::cout << "after stable_sort:\n";
std::ranges::stable_sort(arr);
for (auto& i : arr) std::cout << i << ' ';
std::cout << '\n';
auto [min,max] = std::ranges::minmax(arr);
std::cout << "min: " << min << ", max: " << max << '\n';
return 0;
}

Default reference parameters and lifetimes in coroutines

I'm confused about the lifetime of parameters passed to C++ coroutines.
Answering to a previous question, smart people stated that
The lifetime of a parameter is [...] part of the caller's scope
Now, to follow up, what happens when passing default arguments like
generator my_coroutine(string&& s = string()) {...}
So, if my_coroutine was a normal function, s would be valid throughout its scope. However, this seems to no longer hold if my_coroutine is a coroutine.
In particular the results of the following coroutine-test surprised me:
#include <iostream>
#include <coroutine>
struct Test {
int i = 3;
Test() { std::cout << "test constructed\n";}
Test(const Test&) = delete;
Test(Test&&) = delete;
~Test() { std::cout << "test destructed\n"; }
friend std::ostream& operator<<(std::ostream& os, const Test& t) { return os << t.i; }
};
template<class T>
generator<int> coro_test(T&& t = T()) {
int i = 0;
while(i++ < 3) co_yield i;
if(i == t.i) co_yield 100;
}
int main () {
auto gen = coro_test<Test>();
while(gen.is_valid()) {
std::cout << *gen << "\n";
++gen;
}
return 0;
}
results:
test constructed
test destructed
1
2
3
PS: for completeness, here's my generator:
template<class T>
struct generator {
struct promise_type;
using coro_handle = std::coroutine_handle<promise_type>;
struct promise_type {
T current_value;
auto get_return_object() { return generator{coro_handle::from_promise(*this)}; }
auto initial_suspend() const noexcept { return std::suspend_never{}; }
auto final_suspend() const noexcept { return std::suspend_always{}; }
void unhandled_exception() const { std::terminate(); }
template<class Q>
auto yield_value(Q&& value) {
current_value = std::forward<Q>(value);
return std::suspend_always{};
}
};
private:
coro_handle coro;
generator(coro_handle h): coro(h) {}
public:
bool is_valid() const { return !coro.done(); }
generator& operator++() { if(is_valid()) coro.resume(); return *this; }
T& operator*() { return coro.promise().current_value; }
const T& operator*() const { return coro.promise().current_value; }
generator(const generator&) = delete;
generator& operator=(const generator&) = delete;
~generator() { if(coro) coro.destroy(); }
};
As pointed out in said "previous question", the first thing that happens in a coroutine is that parameters are "copied" into storage owned by the coroutine. However, the "copy" is ultimately initialized based on the type declared in the signature. That is, if a parameter is a reference, then the "copy" of that parameter is also a reference.
So a coroutine function that takes reference parameters is much like any kind of asynchronous function that takes reference parameters: the caller must ensure that the referenced object continues to exist throughout the time that the object will be used. A default parameter which initializes a reference is a circumstance that the caller cannot control the lifetime of (other than providing an explicit parameter).
You created an API that is inherently broken. Don't do that. Indeed, it's best to avoid passing references to async functions of any kind, but if you do, never give them default parameters.

Converting a set of sets to a single range

I'm having trouble with creating a certain type of range.
In LLVM (a compiler infrastructure), a Module (could be the code for a translation unit) is organized as follows:
llvm::Module m{...}; // A module could be a single translation unit.
for(const auto &func : m){
for(const auto &basic_block : func){
for(const auto &inst : basic_block){
}
}
}
In the code I'm working on, it's very convenient to have a function that creates a range of instructions contained in a Module or Function. For example:
llvm::Module m;
llvm::Function f;
InstSet temp;
temp.insert(instructions(m));
temp.insert(instructions(f));
However, I'm having trouble elegantly implementing the range.
Things I tried:
Implementing a custom iterator. It turned out to be really messy even though what I'm trying to do is supposed to be simple...
Implementing using coroutines. While this worked out slightly better, the performance was unacceptable (about 10x slower upon investigation).
My question is, are there any elegant solutions that I'm missing? If there is none, I'd like to know why to understand the problem I'm trying to solve the wrong way.
EDIT (I added my own implementations below):
Here is the implementation that uses coroutines (finished):
struct InstructionsGenerator {
struct promise_type;
using Handle = coroutine_handle<promise_type>;
struct promise_type {
const Instruction *i;
auto get_return_object() -> InstructionsGenerator { return {Handle::from_promise(*this)}; }
auto yield_value(const Instruction &y) -> suspend_always {
i = &y;
return {};
}
auto initial_suspend() -> suspend_never { return {}; };
auto final_suspend() -> suspend_always { return {}; };
auto return_void() -> void {}
};
Handle h;
};
auto instructions_generator(const Module &m) -> InstructionsGenerator {
for (const auto &f : m) {
for (const auto &bb : f) {
for (const auto &i : instructions(bb)) {
co_yield i;
}
}
}
}
struct InstructionRange {
struct iterator {
optional<InstructionsGenerator> g;
auto operator++() -> auto & {
g->h();
return *this;
}
auto operator*() const -> auto & { return *(g->h.promise().i); }
auto operator==(const iterator & /*unused*/) const -> bool { return g->h.done(); }
};
iterator b;
[[nodiscard]] auto begin() const -> iterator { return b; }
[[nodiscard]] auto end() const -> iterator { return {nullopt}; };
~InstructionRange() {
if (b.g) {
b.g->h.destroy();
}
}
InstructionRange(InstructionsGenerator fi) : b{fi} {}
InstructionRange(InstructionRange &&r) noexcept : b{r.b} { r.b.g = nullopt; }
};
auto instructions(const Module &m) -> InstructionRange { return {instructions_generator(m)}; };
Here is the implementation that uses custom iterators (unfinished. Abandoned it after realizing it's too much of a hassle.):
// The real implementation was templated but to give you the idea, I've simplified it.
struct LiftedRange {
llvm::Module& m;
public:
struct iterator {
llvm::Module& m;
llvm::Module::iterator l1;
llvm::Function::iterator l2;
auto operator*(); // Skipped for brevity.
auto operator==(); // Skipped for brevity.
auto operator++(){
if(next(l2) != l1.end()){
++l2;
return *this;
}
do {
++l1;
while(l1->empty() || l1 != m.end()); // Avoid empty sets.
l2 = l1->begin();
return *this;
}
};
auto begin(); // Skipped for brevity.
auto end(); // Skipped for brevity.
};

How to implement a resetting function for custom generator in c++?

I've created a class called zgenerator:
template <typename T>
class zgenerator {
public:
class promise_type;
using handle = std::coroutine_handle<promise_type>;
private:
handle coro;
explicit zgenerator(handle h) : coro{h} {}
public:
// default constructor
zgenerator() noexcept : coro{nullptr} {}
// move constructor and assignment
zgenerator(zgenerator&& other) noexcept : coro{std::move(other.coro)} { }
zgenerator& operator=(zgenerator&& other) noexcept {
coro = std::move(other.coro);
return *this;
}
// deleting copy constructor and assignment
zgenerator(const zgenerator&) = delete;
zgenerator& operator=(const zgenerator&) = delete;
// destructor
~zgenerator() { if (coro) coro.destroy(); }
// promise type
class promise_type {
public:
using value_type = std::remove_reference_t<T>;
using reference_type = std::conditional_t<std::is_reference_v<T>, T, T&>;
using pointer_type = value_type*;
private:
pointer_type m_value;
std::exception_ptr m_except;
public:
zgenerator get_return_object() noexcept {
return zgenerator{handle::from_promise(*this)};
}
static auto get_return_object_on_allocation_failure() {
return zgenerator{nullptr};
}
std::suspend_always initial_suspend() const noexcept { return {}; }
std::suspend_always final_suspend() const noexcept { return {}; }
// no 'co_await'
template <typename T1> std::suspend_never await_transform(T1&&) = delete;
std::suspend_always yield_value(std::remove_reference_t<T>& value) noexcept
requires std::is_rvalue_reference_v<T>
{
m_value = std::addressof(value);
return {};
}
std::suspend_always yield_value(std::remove_reference_t<T>&& value) noexcept {
m_value = std::addressof(value);
return {};
}
void unhandled_exception() {
m_except = std::current_exception();
}
void rethrow_if_exception() {
if (m_except)
std::rethrow_exception(m_except);
}
void return_void() {}
reference_type value() const noexcept {
return static_cast<reference_type>(*m_value);
}
};
// sentinel
struct sentinel {};
// iterator
class iterator {
private:
handle m_coro;
public:
using value_type = typename promise_type::value_type;
using reference = typename promise_type::reference_type;
using pointer = typename promise_type::pointer_type;
using difference_type = std::ptrdiff_t;
using iterator_category = std::input_iterator_tag;
iterator() noexcept : m_coro{nullptr} {}
explicit iterator(handle other) noexcept : m_coro{other} {}
friend bool operator==(const iterator& it, sentinel) noexcept {
return !it.m_coro || it.m_coro.done();
}
friend bool operator==(sentinel s, const iterator& it) noexcept {
return (it == s);
}
iterator& operator++() {
m_coro.resume();
if (m_coro.done()) {
m_coro.promise().rethrow_if_exception();
}
return *this;
}
void operator++(int) {
(void) this->operator++();
}
reference operator*() const noexcept {
return m_coro.promise().value();
}
pointer operator->() const noexcept {
return std::addressof(this->operator*());
}
};
// iterators
iterator begin() {
if (coro) {
coro.resume();
if (coro.done()) {
coro.promise().rethrow_if_exception();
}
}
return iterator{coro};
}
sentinel end() noexcept {
return sentinel{};
}
void swap(zgenerator& other) noexcept {
using std::swap;
swap(coro, other.coro);
}
// manual yielding
T value() { return coro.promise().value(); }
bool next() {
if (coro) {
coro.resume();
return !coro.done();
} else
return false;
}
bool is_done() { return coro.done(); }
};
This is the application:
auto generate_num = [] -> zgenerator<int> {
co_yield 1;
co_yield 3;
co_yield 10;
co_yield -1;
};
auto it = generate_num();
// Used in for-loop:
for (const auto& i : it)
std::cout << i << ' ';
std::cout << '\n';
Output:
1 3 10 -1
I want to reset the object back into the first state. When I tried to reassign a new generator object from it, it throws a runtime error:
it = generate_num();
Expectation:
// reset:
it.reset();
it.next();
while (!it.is_done()) {
std::cout << it.value() << ' ';
it.next();
}
// now printing twice if we include this.
How could I implement a resetting function in my generator?
Your move constructor and assignment need to ensure other.coro doesn't still point to what it used to, otherwise you double-destroy it.
zgenerator(zgenerator&& other) noexcept : coro{std::exchange(other.coro, {})} { }
zgenerator& operator=(zgenerator&& other) noexcept {
std::swap(coro, other.coro);
return *this;
}