December 13, 2021·5 min read·engineeringcppcoroutinesrangeslifetimes

P2502 changes generator defaults to avoid copies on dereference

The December generator proposal continues earlier coroutine work with new template-parameter ordering. Its default reference type avoids an accidental copy on every iterator dereference.

generator<string> looks like a natural way to declare a coroutine producing strings. If the template argument directly determines the iterator's reference type, however, dereferencing that iterator returns a string value. A consumer writing a reference-looking loop can still pay for a copy made inside the iterator. The simple spelling then selects a cost that many users did not intend.

Casey Carter's P2502R0, dated December 13, revises the synchronous generator design around that problem. It continues Lewis Baker and Corentin Jabot's P2168 work, rather than introducing generators to the committee for the first time. Its notable change reorders template parameters and supplies a reference default while retaining separate value and reference types for advanced uses.

I see that default as consequential in a language whose performance claims depend on understanding where objects are copied. Templates can remove runtime abstraction overhead, but the most obvious instantiation should not quietly request an unnecessary value copy. A useful library design makes the ordinary spelling match ordinary expectations without removing more explicit choices.

The first parameter no longer always means dereference by value

The December proposal spells the template parameters as generator<T, Allocator = void, U = void>. When U is omitted, the value type is remove_cvref_t<T>. The reference type is T when T is already a reference, and otherwise const T&. Thus the common generator<string> form has an owning string value type and a const-reference iteration interface.

The earlier P2168R2 proposal describes separate reference and value parameters in another order. Its first parameter directly names the yielded reference type, which need not be a language reference. That generality is useful for proxy objects, but a plain class type can make dereference return a fresh value.

P2502 preserves the general case through the third parameter. When U is supplied, it names the value type and T names the reference type directly. A caller can therefore request a proxy or value-returning iteration interface deliberately. The default does not remove that facility. It changes what the shortest and most common spelling asks for.

The allocator moves to the second parameter. A user specifying an allocator can retain the ordinary value-and-reference behavior without also spelling the advanced reference/value relationship. The design separates two independent customization needs instead of making allocation selection force an explicit reference-policy choice.

Yielded objects remain alive during suspension

The proposal describes a storage strategy based on the yielded expression's lifetime. A glvalue operand of co_yield remains valid while the coroutine is suspended at that yield. The promise can retain a pointer to the yielded object and the iterator can expose the required reference without making an independent copy at each dereference.

When conversion is needed, the coroutine can materialize a suitable temporary associated with the yield. That conversion may still construct an object. Avoiding copies through the iterator does not mean that turning a string literal into a string requires no construction. The proposal distinguishes conversion cost from an unnecessary additional copy during access.

An illustrative producer under the proposed interface is:

#include <generator>
#include <string>

std::generator<std::string> messages()
{
    std::string current = "first";
    co_yield current;
    current = "second";
    co_yield current;
}

This is proposal-based code, requiring an implementation of the described generator. The coroutine reuses a local string, so a consumer retaining a reference has to account for the next resume changing that string. The reference default avoids an implicit copy during dereference. It does not make a reference into suspended computation an independently owned snapshot.

That is why the value type remains useful. A consumer choosing to materialize an owning collection needs to know which values to construct. The paper includes an example exposing string views while identifying strings as the retained value type. A non-owning iteration interface and an owning materialization type can describe different legitimate uses of the same sequence.

Const reference is a deliberate default

P2502 discusses alternatives and rejects a mutable-reference default. Such a default would allow mutation of coroutine-frame objects through iteration and would accept a more restrictive set of yield expressions. Returning an rvalue reference by default introduces other surprises when ordinary-looking consumer code moves from a yielded object.

The authors also reject choosing a special rule based on whether the type is move-only. That would make adding a copy constructor change the interface's meaning. A consistent default based on the explicitly supplied template arguments is easier to reason about than a rule that silently changes when a value type gains an operation.

I prefer the explicit reference form for a producer that intends consumers to move values out. The default communicates observation through const access, and another reference spelling states a different contract. That avoids making an object's copyability decide whether ordinary iteration can modify it.

Move-only ownership of the generator itself remains separate from the yielded reference choice. The generator owns a unique coroutine state, models an input range, and has move-only iterators. Changing the default dereference type does not turn the sequence into a multipass container or make copies of its suspended computation available.

The revision history also removes an earlier concern about constant-time destruction requirements for views, citing the relaxation in P2415R2. A generator can retain objects in its coroutine frame whose destruction has nonconstant work. That observation concerns the semantic requirements for calling the type a view. It is independent of whether advancing or dereferencing its iterator copies a yielded element.

Recursive yielding remains part of the design

The proposal retains ranges::elements_of for yielding another range's elements. It distinguishes that request from yielding a range object as a single value. Recursive generator composition can then use symmetric transfer to resume the active leaf directly, avoiding a parent-by-parent forwarding chain for each produced element.

Exceptions leaving a nested generator return to its parent at the yielding expression, preserving the control-flow interpretation of an explicit loop. Reference selection and recursive transfer therefore address different costs while sharing the same ownership model. The December paper continues both parts of the design, with the changed template ordering making the common reference behavior explicit in the proposed public interface.