如何解决如何通过重载yield_value递归生成生成器?
我已经创建了一个 generator
,它将有一个重载 operator*
以便转换为 std::ranges::subrange
,我还想从 {{1} 重载 yield_value
接受将递归产生的子范围类型。
源代码:
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;
}
};
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
修饰:
yield_value
这样:
在课堂auto generate_1() -> generator<int> {
co_yield* generate_0(10);
}
中:
generator
在嵌套类 auto operator*() {
return std::ranges::subrange(begin(),end());
}
中:
generator<...>::promise_type
解决方法
最重要的事情:你那边的错误/奇数。
- 我认为尝试支持旧式迭代器是不值得的。 没有默认构造
generator<T>::iterator
,并且新式迭代器概念不需要它。您可以从iterator
中撕下大量垃圾。- 另外,
==
很神奇。如果x == y
没有找到匹配的operator==
但y == x
找到了,那么x == y
会自动重写为y == x
。您不需要同时提供两个operator==
。
- 另外,
-
promise_type
不需要按值保存T
。从协程中产生事物的一个奇怪之处在于,如果您让yield_value
接受引用,您可以获得对处于协程状态的事物的引用。但是协程状态会一直保留,直到您恢复它为止!所以promise_type
可以改为持有T const*
。现在,您不再需要T
的可复制性和默认构造性等烦人的东西。 - 最初暂停
generator
似乎是不自然的。目前,如果您执行g.begin(); g.begin();
,即使您没有增加迭代器,您也会推进生成器。如果您使g.begin()
not 恢复协程并删除初始暂停,则一切正常。或者,您可以让generator
跟踪它是否已启动协程,并仅将其推进到begin()
上的第一个收益,但这很复杂。 - 虽然在通常 UB 的每个操作上调用
std::terminate()
可能很好,但它也很吵,我只是不打算将它包含在这个答案中。另外,请不要通过unhandled_exception
调用它。这只是令人困惑:unhandled_exception
有一个非常具体的目的和意义,而您只是不尊重这一点。 -
generator<T>::operator=(generator&&)
泄露了*this
的协程状态!此外,您的swap
是非标准的,因为它不是免费的 2-arg 函数。我们可以通过让operator=
做swap
所做的事情然后去掉swap
来解决这些问题,因为std::swap
可以工作。
从设计/理论的角度来看,我认为实现这种语法更有意义。
auto generate_1() -> generator<int> {
co_await generate_0(10);
}
generator
可以暂时将控制权交给另一个,并可能在 await
内的 generator
耗尽后继续运行。通过使生成器包装范围,可以在此基础上轻松实现从范围中产生的内容。这也与 Haskell 等其他语言的语法一致。
现在,协程没有堆栈。这意味着,一旦我们越过函数调用边界远离像 generate_1
这样的协程,就不可能通过与调用者关联的协程状态挂起/恢复该函数。所以我们必须实现我们自己的堆栈,在那里我们扩展我们的协程状态(promise_type
),以便记录它当前正在从另一个协程中提取而不是拥有自己的值。 (请注意,这也适用于从范围产生:调用任何函数来接收来自 generator_1
的范围将无法控制 generator_1
的协程。)我们通过使 {{ 1}} 保持一个
promise_type
请注意,std::variant<T const*,std::subrange<iterator,std::default_sentinel_t>> value;
不拥有由 promise_type
表示的 generator
。大多数情况下(就像在 subrange
中一样)与 generator_1
相同的技巧适用:拥有子协程状态的 yield_value
位于调用方协程的堆栈中。
(这也是反对从范围直接实现 generator
的一点:我们需要修复进入 co_yield
的任何类型。从 API 的角度来看,{{1} } 在 promise_type
中以接受 co_await
。但是如果我们实现了 generator<T>
,我们将只能直接处理一种特定类型的范围——generator<T>
包装 co_yield
{1}}。那会很奇怪。否则我们需要实现类型擦除;但是在这种情况下对范围进行类型擦除的最明显方法是创建一个 subrange
。所以我们又回到了generator
generator
将另一个作为更基本的操作。)
正在运行的 generator
的堆栈现在是一个链接列表,通过它们的 await
进行线程化。其他一切都是自己写的。
generator
似乎没有任何东西着火。
promise_type
如果你想从任意范围产生值,我会实现这个类型橡皮擦。
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!
}
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。