ericniebler / range-v3

Range library for C++14/17/20, basis for C++20's std::ranges
Other
4.1k stars 441 forks source link

P2168 style generator fails to compile when used as pipe sequence source. #1653

Open melton1968 opened 2 years ago

melton1968 commented 2 years ago

I am using clang-11 with the latest version of range-v3 and a generator implementation that follows P2168. Should this in principle work?

The code:

#include <range/v3/range.hpp>
template <typename T, typename U>
constexpr inline bool ranges::enable_view<coro::Generator<T,U>> = true;

coro::Generator<const int> counter(int n) {
    for (auto i = 0; i < n; ++i)
     co_yield i;
}

TEST(CoroGenerator, Ranges)
{
  auto g = counter(20) | v::take(5);
  for (auto i : g)
    cout << i << endl;
}

generates the following completion error:

/Users/mmelton/work/cxx-trade/extern/cxx-core/test/src/coro/test_generator.cpp:75:26: error:
      invalid operands to binary expression ('coro::Generator<const int>' and
      'ranges::views::view_closure<ranges::detail::bind_back_fn_<ranges::views::take_base_fn,
      int>>')
    auto g = counter(20) | v::take(5);
             ~~~~~~~~~~~ ^ ~~~~~~~~~~
/Users/mmelton/work/cxx-depends/build/cxx-depends-install/include/range/v3/view/view.hpp:116:17: no\
te:
      candidate template ignored: constraints not satisfied [with Rng = coro::Generator<const int,
      int>, ViewFn = ranges::detail::bind_back_fn_<ranges::views::take_base_fn, int>]
                operator|(Rng && rng, view_closure<ViewFn> vw)
                ^
/Users/mmelton/work/cxx-depends/build/cxx-depends-install/include/range/v3/view/view.hpp:113:33: no\
te:
      because 'coro::Generator<const int, int>' does not satisfy 'viewable_range'
                requires defer::viewable_range<Rng> &&          //
                                ^
/Users/mmelton/work/cxx-depends/build/cxx-depends-install/include/concepts/concepts.hpp:284:27: not\
e:
      expanded from macro 'CPP_template'
    template<__VA_ARGS__> CPP_PP_EXPAND                                         \
                          ^
/Users/mmelton/work/cxx-depends/build/cxx-depends-install/include/range/v3/range/concepts.hpp:387:5\
6: note:
      because 'coro::Generator<const int, int>' does not satisfy 'viewable_range'
        CPP_concept viewable_range = CPP_defer(ranges::viewable_range, T);
                                                       ^
/Users/mmelton/work/cxx-depends/build/cxx-depends-install/include/range/v3/range/concepts.hpp:288:9\
: note:
      because 'coro::Generator<const int, int>' does not satisfy 'range'
        range<T> &&
        ^
/Users/mmelton/work/cxx-depends/build/cxx-depends-install/include/range/v3/range/concepts.hpp:88:9:\
 note:
      because 'coro::Generator<const int, int> &' does not satisfy 'range_impl_'
        range_impl_<T &>;
        ^
/Users/mmelton/work/cxx-depends/build/cxx-depends-install/include/range/v3/range/concepts.hpp:82:13\
: note:
      because 'ranges::begin(((decltype(t) &&)t)) , ranges::end(((decltype(t) &&)t))' would be
      invalid: no matching function for call to object of type 'const _begin_::fn'
            ranges::begin(CPP_fwd(t)), // not necessarily equality-preserving

The generator implementation:

#pragma once
#include <experimental/coroutine>
#include "core/common.h"

namespace coro
{

// (possibly recusrive) Generator using symmetric transfer.
//
template<class Reference, class Value = std::remove_cvref_t<Reference>>
class Generator {
public:
    using always = std::experimental::suspend_always;
    using never = std::experimental::suspend_never;

    class promise_type;
    using handle_type = std::experimental::coroutine_handle<promise_type>;
    using generic_handle_type = std::experimental::coroutine_handle<>;

    class promise_type {
    public:
        promise_type() : root_or_leaf_(this) { }

        Generator get_return_object() noexcept {
            return Generator(handle_type::from_promise(*this));
        }

        void unhandled_exception() {
            if (exception_ == nullptr)
                throw;
            *exception_ = std::current_exception();
        }

        // co_return is a noop.
        void return_void() { }

        // Always suspend initially.
        always initial_suspend() { return {}; }

        // final_awaiter is returned when the generator is
        // exhausted. If this is an inner generator, symmetric
        // transfer is used to return control back to the parent
        // generator. Otherwise, control is returned to the caller.
        struct final_awaiter {
            // We always suspend.
            bool await_ready() noexcept { return false; }

            // If there is a parent generator, use symmetic transfer
            // to invoke the parent coroutine; otherwise, return to
            // the caller.
            generic_handle_type await_suspend(handle_type coro) noexcept {
                auto& promise = coro.promise();
                auto parent = coro.promise().parent_;
                if (parent) {
                    // Keep the root pointing to the leaf (most
                    // nested) promise which the parent is about to
                    // become as we pop-up one level of "recursion"
                    // using symmetric transfer.
                    promise.root_or_leaf_->root_or_leaf_ = parent;
                    return handle_type::from_promise(*parent);
                }
                return std::experimental::noop_coroutine();
            }
            void await_resume() noexcept { }
        };

        // Return the final_awaiter.
        final_awaiter final_suspend() noexcept { return {}; }

        // Record the address of the yieled value.
        always yield_value(Reference&& v) noexcept {
            root_or_leaf_->value_ = std::addressof(v);
            return {};
        }

        // Record the address of the yielded value.
        always yield_value(Reference& v) noexcept {
            root_or_leaf_->value_ = std::addressof(v);
            return {};
        }

        // yield_sequence_awaiter is returned when a Generator is
        // yielded from within a Generator.
        struct yield_sequence_awaiter {
            Generator generator_;
            std::exception_ptr exception_;

            // Taking ownership of the nested generator ensures frames
            // are destroyed in reverse order of creation.
            explicit yield_sequence_awaiter(Generator&& generator) noexcept
                : generator_(std::move(generator)) {
            }

            // If the nested generator is exhausted, we skip
            // suspending and continue with the containing generator
            // yielding no values.
            bool await_ready() noexcept {
                return not generator_.coro_;
            }

            // If the nested generator is non-empty, we do the
            // bookkeeping necessary to return to this generator later
            // and use symmetric transfer to invoke the inner
            // generator coroutine.
            generic_handle_type await_suspend(handle_type coro) noexcept {
                auto& current = coro.promise();
                auto& nested = generator_.coro_.promise();
                auto& root = current.root_or_leaf_;

                // The leaf (most nested) promise always points to the
                // root promise.
                nested.root_or_leaf_ = root;

                // The root promise always points to the leaf (most
                // nested) promise.
                root->root_or_leaf_ = &nested;

                // Parent tracks the immediate parent promise.
                nested.parent_ = &current;
                nested.exception_ = &exception_;

                return generator_.coro_;
            }

            // If there was an exception, throw it.
            void await_resume() {
                if (exception_)
                    std::rethrow_exception(std::move(exception_));
            }
        };

        // Yielding a generator. We transfer ownership to the awaiter
        // so that coroutine lifetimes are appropriately nested.
        yield_sequence_awaiter yield_value(Generator&& generator) noexcept {
            return yield_sequence_awaiter{std::move(generator)};
        }

        // Resume the active coroutine.
        void resume() {
            handle_type::from_promise(*root_or_leaf_).resume();
        }

        // Disable use of co_await within generators.
        void await_transform() = delete;

    private:
        friend Generator;

        // Pointer to the promise for either the root generator (if
        // this is a leaf generator) or the current leaf generator (if
        // this is not a leaf generator). If this is both the root and
        // leaf generator (i.e. non-recursive), then it points to
        // `this` which is both.
        promise_type *root_or_leaf_;
        // Pointer to the promise for the parent generator, if any.
        promise_type *parent_ = nullptr;
        std::exception_ptr *exception_ = nullptr;
        std::add_pointer_t<Reference> value_;
    };

    struct sentinel {};

    struct iterator {
    public:
        using iterator_category = std::input_iterator_tag;
        using difference_type = std::ptrdiff_t;
        using value_type = Value;
        using reference = Reference;
        using pointer = std::add_pointer_t<Reference>;

        iterator() noexcept = default;

        iterator(const iterator&) = delete;

        iterator(iterator&& other) {
            std::swap(coro_, other.coro_);
        }

        iterator& operator=(iterator&& other) {
            std::swap(coro_, other.coro_);
            return *this;
        }

        ~iterator() { }

        friend bool operator==(const iterator& iter, sentinel) noexcept {
            return not iter.coro_ or iter.coro_.done();
        }

        friend bool operator!=(const iterator& iter, sentinel s) noexcept {
            return not (iter == s);
        }

        friend bool operator==(sentinel s, const iterator& iter) noexcept {
            return iter == s;
        }

        friend bool operator!=(sentinel s, const iterator& iter) noexcept {
            return iter != s;
        }

        iterator& operator++() {
            coro_.promise().resume();
            return *this;
        }

        void operator++(int) {
            operator++();
        }

        reference operator*() const noexcept {
            return static_cast<reference>(*coro_.promise().value_);
        }

        pointer operator->() const noexcept
            requires std::is_reference_v<reference> {
            return std::addressof(operator*());
        }

    private:
        friend Generator;

        explicit iterator(handle_type coro) noexcept
            : coro_(coro) {
        }

        handle_type coro_;
    };

    Generator() noexcept = default;
    Generator(const Generator& other) = delete;

    Generator(Generator&& other) noexcept
        : coro_(std::exchange(other.coro_, {})) {
    }

    Generator& operator=(Generator& other) noexcept {
        std::swap(coro_, other.coro_);
        return *this;
    }

    ~Generator() {
        if (coro_)
            coro_.destroy();
    }

    // Return false iff the generator is exhausted (i.e. there are no
    // more values to be yielded).
    bool next() {
        coro_.promise().resume();
        return coro_ and not coro_.done();
    }

    // Return the next value from the generator.
    Reference operator()() const {
        return static_cast<Reference>(*coro_.promise().value_);
    }

    // Return the begin iterator resuming the coroutine which will
    // populate the promise with the first value.
    iterator begin() {
        if (coro_)
            coro_.resume();
        return iterator{coro_};
    }

    // Return the end iterator.
    sentinel end() {
        return {};
    }

private:
    // This should only be called by the promise.
    Generator(handle_type coro)
        : coro_(coro)
    { }

    handle_type coro_{nullptr};
};

}; // coro
melton1968 commented 2 years ago

I implemented generator_view (modeled from istream_view) which allows me to write the following code:

coro::Generator<int> counter(int n) {
    for (auto i = 0; i < n; ++i)
        co_yield i;
}

auto g = ranges::generator_view(counter(7));
auto h = g | v::take(5);
for (auto s : h)
    cout << s << endl;

Feedback on whether this is a good path is appreciated. Maybe there is better view to use as a model?

The implementation of generator_view follows:

namespace ranges
{

template<class T, class U = std::remove_cvref_t<T>>
struct generator_view : view_facade<generator_view<T, U>, unknown> {
private:
    friend range_access;
    coro::Generator<T, U> gen_;
    semiregular_box_t<U> obj_;

    struct cursor {
    private:
        friend range_access;
        using single_pass = std::true_type;
        generator_view *rng_ = nullptr;

    public:
        cursor() = default;
        explicit cursor(generator_view *rng)
            : rng_(rng) {
        }

        void next() { rng_->next(); }
        T& read() const noexcept { return rng_->cached(); }
        bool equal(default_sentinel_t) const { return rng_->gen_.done(); }
    };

    void next() {
        gen_.next();
        cached() = gen_();
    }

    cursor begin_cursor() { return cursor{this}; }

public:
    generator_view() = default;
    explicit generator_view(coro::Generator<T>&& gen)
        : gen_(std::move(gen)) {
        next();
    }

    T& cached() noexcept { return obj_; }
};

namespace _generator_
{

template<class T, class U = std::remove_cvref_t<T>>
requires copy_constructible<U> && default_constructible<U>
inline generator_view<T,U> generator(coro::Generator<T,U>&& gen) {
    return generator_view<T,U>{std::move(gen)};
}

}; // _generator_

using namespace _generator_;

}; // ranges