如何通过重载yield_value来递归生成生成器?

2024-01-01

我创建了一个generator将会出现过载operator*为了转换成std::ranges::subrange我也想超载yield_value from promise_type接受将递归生成的子范围类型。

源代码:

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显然会工作,但我希望有相同的输出generate_1每个元素都是co_yield-ed 直接在yield_value:

auto generate_1() -> generator<int> {
  co_yield* generate_0(10);
}

这样: 在班上generator:

auto operator*() {
      return std::ranges::subrange(begin(), end());
}

在嵌套类中generator<...>::promise_type:

template <typename U>
std::suspend_always yield_value(const std::ranges::subrange<U, std::default_sentinel_t>& r) noexcept {
  /** ... **/
  return {};
}

首先要做的事情是:你这边的错误/奇怪的地方。

  • 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, ==很神奇。如果x == y没有找到匹配的operator== but y == x那么x == y被自动重写为y == x。您无需同时提供两者operator==s.
  • The promise_type不需要持有T按价值。从协程中产生东西的一个奇怪的事情是,如果你做了yield_value通过引用,您可以获得对协程状态中某些内容的引用。但协程状态会一直保留,直到您恢复它为止!所以promise_type可以改为持有T const*。现在您不再需要烦人的东西,例如可复制性和默认可构造性T.
  • 对于一个人来说这似乎是不自然的generator最初暂停。目前,如果您这样做g.begin(); g.begin();,即使您没有增加任何迭代器,您也会推进生成器。如果你做g.begin() not恢复协程并删除最初的暂停,一切正常。或者,你可以使generator跟踪它是否已启动协程并仅将其推进到第一个yieldbegin(),但这很复杂。
  • 打电话时std::terminate()通常 UB 的每个操作可能都很好,但它也很吵,我只是不打算将其包含在这个答案中。还,please不要通过调用它unhandled_exception。这只是令人困惑:unhandled_exception有一个非常具体的目的和意义,而你只是不尊重这一点。
  • generator<T>::operator=(generator&&) leaks *this的协程状态!另外,你的swap是非标准的,因为它不是一个免费的 2-arg 函数。我们可以通过以下方式解决这些问题operator=做什么swap做了然后摆脱swap因为std::swap works.

从设计/理论的角度来看,我认为实现这种语法更有意义。

auto generate_1() -> generator<int> {
  co_await generate_0(10);
}

A generator可以暂时将控制权交给另一个人,并可以在它之后恢复运行awaits 为内部generator耗尽。通过使生成器包装范围,可以轻松地实现从范围生成的内容。这也与 Haskell 等其他语言的语法一致。

现在,协程没有堆栈。这意味着一旦我们跨越函数调用边界远离像这样的协程generate_1,不可能通过与调用者关联的协程状态来挂起/恢复该函数。所以我们必须实现我们自己的堆栈,在其中扩展我们的协程状态(promise_type)能够记录它当前正在从另一个协程中拉取,而不是拥有自己的值。 (请注意,这也适用于从范围中产生:调用任何函数来接收范围generator_1将无法控制generator_1的协程。)我们通过以下方式做到这一点promise_type hold a

std::variant<T const*, std::subrange<iterator, std::default_sentinel_t>> value;

注意promise_type才不是own the generator代表为subrange。大多数时候(就像在generator_1)同样的技巧yield_value适用:generator拥有子协程状态的子协程状态位于调用者协程堆栈内。

(这也是反对直接实施的一点co_yield从范围内:我们需要修复进入的任何内容的类型promise_type。从API的角度来看,这是可以理解的co_await里面一个generator<T>接受generator<T>s。但如果我们实施co_yield我们只能直接处理一种特定的范围——asubrange包裹一个generator。那会很奇怪。否则我们需要实现类型擦除;但在这种情况下,键入擦除范围的最明显方法是创建一个generator。所以我们回到了generator await认为另一个是更基本的操作。)

运行时的堆栈generators 现在是一个链接列表,通过它们promise_types。其他一切都只是自己写。

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

似乎没有什么东西着火了。

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!
}

如果你想产生任意范围的值,我只需实现这个类型擦除器。

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

所以你得到例如

generator<unsigned> generate_1(unsigned n) {
    co_await generate_all(std::array{41u, 42u, 43u});
    co_await generate_0(n);
    co_yield 0;
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何通过重载yield_value来递归生成生成器? 的相关文章

随机推荐