【问题标题】:How to recursively yield generator by overloading yield_value?如何通过重载yield_value递归生成生成器?
【发布时间】:2021-10-07 01:10:23
【问题描述】:

我创建了一个generator,它将有一个重载operator*,以便转换为std::ranges::subrange,我还想从promise_type 重载yield_value,它接受一个将产生的子范围类型递归。

源代码:

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

例子:

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&lt;...&gt;::promise_type:

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

【问题讨论】:

    标签: c++ generator c++-coroutine


    【解决方案1】:

    首先要做的事:你的错误/奇怪的位。

    • 我认为尝试支持旧式迭代器是不值得的。它没有默认构造generator&lt;T&gt;::iterator 有意义,并且新型迭代器概念不需要它。你可以从iterator 中撕掉很多垃圾。
      • 另外,== 很神奇。如果x == y 没有找到匹配的operator==y == x 找到了,那么x == y 会自动重写为y == x。您无需同时提供operator==s。
    • promise_type 不需要按值保存T。从协程中产生东西的一个奇怪的事情是,如果你让yield_value 引用引用,你可以获得对生活在协程状态下的东西的引用。但是协程状态会一直保留,直到您恢复它!所以promise_type 可以改为持有T const*。现在,您不再需要来自T 的可复制性和默认可构造性等烦人的东西。
    • generator 最初挂起似乎是不自然的。目前,如果您执行g.begin(); g.begin();,即使您没有增加迭代器,您也会推进生成器。如果你让g.begin()not 恢复协程并移除初始暂停,一切正常。或者,您可以让 generator 跟踪它是否已启动协程,并仅将其推进到 begin() 上的第一个 yield,但这很复杂。
    • 虽然在通常是 UB 的每个操作上调用 std::terminate() 可能很好,但它也很吵,我只是不打算将它包含在这个答案中。另外,不要通过unhandled_exception 调用它。这只是令人困惑:unhandled_exception 有一个非常具体的目的和意义,而你只是不尊重这一点。
    • generator&lt;T&gt;::operator=(generator&amp;&amp;) 泄露 *this 的协程状态!此外,您的 swap 是非标准的,因为它不是免费的 2-arg 函数。我们可以通过让operator=swap 所做的事情然后摆脱swap 来解决这些问题,因为std::swap 有效。

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

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

    generator 可以暂时放弃控制权,并可以在awaits 之后恢复运行,以使内部generator 耗尽。通过使生成器包装范围,可以轻松地在此之上实现从范围产生的东西。这也符合 Haskell 等其他语言的语法。

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

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

    请注意,promise_type拥有subrange 表示的generator。大多数时候(就像在generator_1 中一样)与yield_value 相同的技巧适用:拥有子协程状态的generator 位于调用方协程的堆栈中。

    (这也是反对从一个范围直接实现co_yield 的一点:我们需要修复进入promise_type 的任何内容的类型。从API 的角度来看,co_awaitgenerator&lt;T&gt; 中是可以理解的接受generator&lt;T&gt;s。但是如果我们实现co_yield,我们只能直接处理一种特定的范围——一个subrange包装一个generator。这很奇怪。否则我们'd 需要实现类型擦除;但在这种情况下对范围进行类型擦除最明显的方法是创建一个 generator。所以我们回到另一个 generator awaiting 作为更基本的操作。)

    正在运行的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;
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-10-18
      • 2020-11-04
      • 2019-08-25
      • 2021-05-22
      • 2020-08-04
      • 2016-05-03
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多