Date: Wed, 27 Aug 2025 08:11:27 +0800 (GMT+08:00)
Arthur: Yes, I think all of these stuffs are introduced in C++23.
Brian: I think you're right, this problem is slightly more complex than I previously expected. So when we write code like:
std::generator<int> TestElem()
{
int a = 1;
co_yield a;
}
There actually exists a copy (from lvalue `a` to awaiter), and promise of generator stores pointer to copy in awaiter. The first type parameter means "how users want to treat return value of operator*", and by specifying int, it means operator* should return int&& and thus lvalue has to be copied to keep the original value as is when resumed.
So natually, for code that yields a lvalue range:
std::generator<int> TestRange()
{
std::vector<int> vec{ 0, 1, 2 };
co_yield std::ranges::elements_of(vec);
}
Each element should be copied too instead of static_cast to int&&. So I think a possible fix is to remove constraint + remove static_cast in specification. And when users really want to treat all elements as rvalue, then they can add `| std::as_rvalue` explicitly as Arthur points out.
Besides, this fix seems to introduce another possible performance pitfall. For code below:
std::generator<int> TestTempRange()
{
co_yield std::ranges::elements_of(std::vector<int>{ 0, 1, 2 });
}
It may be desired to automatically treat elements of this vector as xvalue instead of doing copy, as the vector itself is rvalue. Generally speaking, it may be fixed by adding `| std::views::all` and take special care for `owning_view`. (I think there may already be some utility that I'm unaware of to do so in the standard library).
Liang Jiaming
----------------------------------------------------
From: Brian Bi <bbi5291_at_[hidden]om>
Date: 2025-08-27 00:30:10
To: std-proposals_at_[hidden]cpp.org
Title:Re: [std-proposals] Possible DR for successive behavior of std::generator
On Tue, Aug 26, 2025 at 10:51 AM 梁家铭 via Std-Proposals <std-proposals_at_[hidden]> wrote:
Hi,
Recently I find that it's illegal to write std::generator code like:
std::generator<int> Test()
{
std::vector<int> vec{ 0, 1, 2 };
co_yield std::ranges::elements_of(vec);
}
which is very counter-intuitive. This code example is even adopted from the std::generator proposal (P2502R2, page 16), which also thinks it should be legal. In other words, the proposal seems to be inconsistent in intention and wording, so I think there should be a DR to fix it.
Let me briefly analyze what happens first. Remember that here std::generator<int>::yielded == int&&.
So in current specification of std::generator, the standard regulates that for .yield_value() of the promise type (see [coro.generator]):
Effects: Equivalent to:auto nested = [](allocator_arg_t, Alloc, ranges::iterator_t<R> i, ranges::sentinel_t<R> s) -> generator<yielded, void, Alloc> { for (; i != s; ++i) { co_yield static_cast<yielded>(*i); } }; return yield_value(ranges::elements_of(nested( allocator_arg, r.allocator, ranges::begin(r.range), ranges::end(r.range))));
So here it's legal, since static_cast<yielded>(*i) (i.e.static_cast<int&&>(int&)) is legal. However, the constraints of .yield_value() reject it:
template<ranges::input_range R, class Alloc> requires convertible_to<ranges::range_reference_t<R>, yielded> auto yield_value(ranges::elements_of<R, Alloc> r);
Here ranges::range_reference_t<R> is int&, and yielded is int&&. That is, int& is explicitly convertible to int&& (which is fine for std::generator), but not implicitly convertible to int&&, making the constraint fail.
So I think there should be a DR to loosen the constraint to make explicit conversion enough.
Using `std::is_constructible_v` for the constraint would already be questionable: it would be able to call explicit constructors and explicit conversion functions. But that still wouldn't be enough for what you're asking for. Conversion from `int&` to `int&&` requires an explicit cast. I do not think `yield_value` should accept ranges whose reference type has to be explicitly casted to the yielded type. That would allow all sorts of questionable conversions, like `Base*` to `Derived*`.
However, I agree that there does seem to be a problem here. We can `co_yield` an lvalue or rvalue of type `int` from this generator coroutine, so the fact that this code doesn't compile is surprising and seems unintended. `co_yield`ing an lvalue here is specifically supported by the following overload of `yield_value`:
auto yield_value(const remove_reference_t<yielded>& lval)
requires is_rvalue_reference_v<yielded> &&
constructible_from<remove_cvref_t<yielded>, const remove_reference_t<yielded>&>;
It seems to me that the constraint for the range overload ought to be consistent with the single-value case: if either of the two single-value overloads is viable (or if both are viable and the overload resolution is unambiguous) then the range ought to be accepted.
[This email is also sent to author of P2502]
Liang Jiaming
Brian: I think you're right, this problem is slightly more complex than I previously expected. So when we write code like:
std::generator<int> TestElem()
{
int a = 1;
co_yield a;
}
There actually exists a copy (from lvalue `a` to awaiter), and promise of generator stores pointer to copy in awaiter. The first type parameter means "how users want to treat return value of operator*", and by specifying int, it means operator* should return int&& and thus lvalue has to be copied to keep the original value as is when resumed.
So natually, for code that yields a lvalue range:
std::generator<int> TestRange()
{
std::vector<int> vec{ 0, 1, 2 };
co_yield std::ranges::elements_of(vec);
}
Each element should be copied too instead of static_cast to int&&. So I think a possible fix is to remove constraint + remove static_cast in specification. And when users really want to treat all elements as rvalue, then they can add `| std::as_rvalue` explicitly as Arthur points out.
Besides, this fix seems to introduce another possible performance pitfall. For code below:
std::generator<int> TestTempRange()
{
co_yield std::ranges::elements_of(std::vector<int>{ 0, 1, 2 });
}
It may be desired to automatically treat elements of this vector as xvalue instead of doing copy, as the vector itself is rvalue. Generally speaking, it may be fixed by adding `| std::views::all` and take special care for `owning_view`. (I think there may already be some utility that I'm unaware of to do so in the standard library).
Liang Jiaming
----------------------------------------------------
From: Brian Bi <bbi5291_at_[hidden]om>
Date: 2025-08-27 00:30:10
To: std-proposals_at_[hidden]cpp.org
Title:Re: [std-proposals] Possible DR for successive behavior of std::generator
On Tue, Aug 26, 2025 at 10:51 AM 梁家铭 via Std-Proposals <std-proposals_at_[hidden]> wrote:
Hi,
Recently I find that it's illegal to write std::generator code like:
std::generator<int> Test()
{
std::vector<int> vec{ 0, 1, 2 };
co_yield std::ranges::elements_of(vec);
}
which is very counter-intuitive. This code example is even adopted from the std::generator proposal (P2502R2, page 16), which also thinks it should be legal. In other words, the proposal seems to be inconsistent in intention and wording, so I think there should be a DR to fix it.
Let me briefly analyze what happens first. Remember that here std::generator<int>::yielded == int&&.
So in current specification of std::generator, the standard regulates that for .yield_value() of the promise type (see [coro.generator]):
Effects: Equivalent to:auto nested = [](allocator_arg_t, Alloc, ranges::iterator_t<R> i, ranges::sentinel_t<R> s) -> generator<yielded, void, Alloc> { for (; i != s; ++i) { co_yield static_cast<yielded>(*i); } }; return yield_value(ranges::elements_of(nested( allocator_arg, r.allocator, ranges::begin(r.range), ranges::end(r.range))));
So here it's legal, since static_cast<yielded>(*i) (i.e.static_cast<int&&>(int&)) is legal. However, the constraints of .yield_value() reject it:
template<ranges::input_range R, class Alloc> requires convertible_to<ranges::range_reference_t<R>, yielded> auto yield_value(ranges::elements_of<R, Alloc> r);
Here ranges::range_reference_t<R> is int&, and yielded is int&&. That is, int& is explicitly convertible to int&& (which is fine for std::generator), but not implicitly convertible to int&&, making the constraint fail.
So I think there should be a DR to loosen the constraint to make explicit conversion enough.
Using `std::is_constructible_v` for the constraint would already be questionable: it would be able to call explicit constructors and explicit conversion functions. But that still wouldn't be enough for what you're asking for. Conversion from `int&` to `int&&` requires an explicit cast. I do not think `yield_value` should accept ranges whose reference type has to be explicitly casted to the yielded type. That would allow all sorts of questionable conversions, like `Base*` to `Derived*`.
However, I agree that there does seem to be a problem here. We can `co_yield` an lvalue or rvalue of type `int` from this generator coroutine, so the fact that this code doesn't compile is surprising and seems unintended. `co_yield`ing an lvalue here is specifically supported by the following overload of `yield_value`:
auto yield_value(const remove_reference_t<yielded>& lval)
requires is_rvalue_reference_v<yielded> &&
constructible_from<remove_cvref_t<yielded>, const remove_reference_t<yielded>&>;
It seems to me that the constraint for the range overload ought to be consistent with the single-value case: if either of the two single-value overloads is viable (or if both are viable and the overload resolution is unambiguous) then the range ought to be accepted.
[This email is also sent to author of P2502]
Liang Jiaming
-- Std-Proposals mailing list Std-Proposals_at_[hidden] https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals -- Brian Bi
Received on 2025-08-27 00:11:40