首先要做的事情是:你这边的错误/奇怪的地方。
- 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
可以暂时将控制权交给另一个人,并可以在它之后恢复运行await
s 为内部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
认为另一个是更基本的操作。)
运行时的堆栈generator
s 现在是一个链接列表,通过它们promise_type
s。其他一切都只是自己写。
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;
}