March 22, 2021·4 min read·engineeringcppcoroutinesrangesallocation

P2168 makes recursive coroutine generators compatible with ranges

The generator proposal connects coroutine suspension to single-pass range iteration. Recursive yielding and allocator support reveal the ownership and ABI decisions behind that interface.

A coroutine that yields values is not yet a standard range. Consumers still need an iterator interface, and someone must own the suspended coroutine state. A recursive producer adds another problem. If every parent resumes its child and yields the child's value again, producing one leaf value can require work at every level of the recursion.

Lewis Baker and Corentin Jabot address those issues in P2168R2, dated March 22. The paper proposes std::generator as a synchronous coroutine generator compatible with ranges. R2 updates wording and implementation material and expands the allocator discussion. It remains a proposal, with an interface and implementation tradeoffs under review.

I see the ownership model as a strong reason for a standard facility. RAII is one of C++'s most useful mechanisms, and suspended computation needs a clear owner just as an open file does. The proposal makes the coroutine state a unique resource instead of pretending that copying a handle creates an independent sequence.

Iteration resumes a unique computation

The proposed generator is a non-copyable view modeling an input range, with move-only iterators. That is a single-pass model. Advancing the sequence resumes computation and can change the storage holding the currently yielded value. A consumer must not assume the independent traversal behavior of a multipass container.

The paper distinguishes the iterator's reference type from its value type. The reference type describes what dereferencing iteration exposes, while a separately chosen value type can describe what a consumer should retain. A generator can expose a string view during iteration but identify string as the owning value type for materialization.

That distinction matters when the generator reuses a local string between yields. Keeping string views into that changing storage would not produce an independently owned collection. The paper's example uses the value-type parameter to give collection code the information needed to copy strings. A lazy interface still has to describe ownership when a consumer decides to retain results.

The January range-formatting proposal identifies another consequence of this model. A move-only range without const iteration cannot always pass through a formatting interface built around const references. Generators therefore affect the interfaces that consume ranges, not only the syntax used by producers.

Recursive yielding avoids forwarding through every parent

P2168 proposes elements_of to distinguish yielding a range's elements from yielding the range object as a single value. Without that marker, a generator whose yielded type can contain another generator would make the intention ambiguous. The explicit wrapper gives the operation an identifiable meaning.

For a tree traversal, the producer can yield the left subtree's elements, then its current value, then the right subtree's elements. The proposal describes symmetric transfer as the way to resume the active leaf generator directly. A manual loop in every parent instead needs a chain of resumptions and suspensions for each leaf value, with cost proportional to nesting depth.

The exception behavior preserves the manual-loop interpretation. An exception leaving a nested generator is rethrown into its parent at the yielding expression. The parent can catch it there and continue yielding other values. The design therefore optimizes how control reaches the leaf without discarding the exception structure a programmer would expect from explicit iteration.

The authors favor one generator type supporting recursive yielding over separate ordinary and recursive types. Choosing a separate non-recursive type too early can make later composition require a forwarding adapter. The proposed single type avoids forcing a producer to predict whether some future caller will place it inside a recursive traversal.

Allocation choices affect the public type

Coroutine state may require allocation, and R2 develops support for both stateless and stateful allocators. The proposed function convention places an allocator after allocator_arg_t. This distinguishes an allocator intended for the coroutine frame from one merely passed for use inside the body.

The default generator type erases the allocator type. Other forms put a chosen allocator in the generator's template parameters. A stateful allocator must remain available with the coroutine state so deallocation can use the appropriate allocator. These choices affect storage and the representation required to destroy the frame.

I prefer the paper's decision to address this before standardizing the type. Its allocator discussion notes that adding the template parameter later would break the ABI. Deferring allocation policy is therefore not just postponing a convenience function. It can constrain how an already-published type can evolve.

The performance discussion is correspondingly specific. Reported results depend on optimization of coroutine allocation and inlining, and the authors caution against comparing benchmark numbers across the different hardware used. The architectural claim is about avoiding repeated traversal of a recursive resumption chain. Frame ownership and allocation still have costs, which the interface exposes for a standard-library implementation to manage.