Date: Sun, 8 Jun 2025 20:22:00 +0100
On Sun, 8 Jun 2025 at 11:22, Stewart Becker <stewart_at_[hidden]> wrote:
>
> On 07/06/2025 22:09, Jonathan Wakely wrote:
>
>
>
> On Sat, 7 Jun 2025, 20:26 Stewart Becker, <stewart_at_[hidden]> wrote:
>
>> Thank you, Jonathan for your insights.
>> On 07/06/2025 15:35, Jonathan Wakely wrote:
>>
>>
>>
>> On Sat, 7 Jun 2025 at 15:15, Jonathan Wakely <cxx_at_[hidden]> wrote:
>>
>>>
>>>
>> N.B. calling future::get() doesn't necessarily mean that calling it again
>> is UB. It's specified to change valid() to false, but whether that's UB is
>> up to the implementation, and recommended practice is to diagnose it by
>> throwing, so no UB.
>>
>> According to https://en.cppreference.com/w/cpp/thread/future/get,
>> calling get() when valid() is false is UB. The standard doesn't say this
>> explicitly, but it does say that get() retrieves the value/throws the
>> exception stored in the shared state, which doesn't exist when valid() is
>> false. Thus the behaviour of get() when valid() is false is literally
>> undefined. But I concede that it's not called out as UB either. Perhaps
>> this needs addressing in a separate issue?
>>
>
> It says it here:
>
> https://eel.is/c++draft/futures#unique.future-3
>
> But it also recommends that it should be defined to throw by
> implementions. So they should check for the "no state" case and throw, not
> just crash or set fire to your toes.
>
> And that's why I think that the right way to phrase the behaviour of your
> proposal is in terms of whether valid() is true or false. Because then
> paragraph 3 days what happens (and depending on the implementation, it
> might not be UB).
>
> Thank you for the pointer. You've already convinced me that defining the
> behaviour in terms of valid() is true is the way forward.
>
> Calling std::future<T>::get() sets valid() to false, even for
>> future<void>, because the future releases the shared state as a side-effect
>> of calling get(). The "result" may not be changed, but the future itself
>> is. We absolutely don't want get_exception() to invalidate the future - at
>> least when there is a good result in the shared state, as we need to be
>> able to call get() later. This is less important when there is an
>> exception rather than a value in the shared state, but for get_exception()
>> to invalidate the future in some cases but not in others would complicate
>> matters to no obvious benefit.
>>
>
> Agreed. And exception_ptr is already reference counted so if you get a
> copy of the future's exception_ptr, it remains valid even if the future
> goes away. OK, you've convinced me.
>
> Thank you.
>
>
> I'm probably just being unimaginative but I don't see why you'd want to
> stay in the "future space" and return another future when you've already
> extracted the result (or the exception) from it. Doesn't that just add more
> overhead to have to get the result (or exception) out of the future again?
>
> I suppose for the or_else case, the future you return for the exceptional
> case might not be ready yet, so allows more asynchronous work.
>
> But for the when_any case, don't you end up waiting for a result, and so
> there's no more asynchronous work? It always returns a ready future, so
> could just return the result instead. I suppose staying in the "future
> space" allows you to combine the returned future into another when_any
> call, or some other monad working with futures.
>
> Exactly. In a word, the reason for staying in future-space is composition.
>
Ack.
> Also bear in mind that futures aren't only used for asynchronous work,
> they can also be used for deferred, sychronous work.
>
Sure, but again, your when_any would execute any deferred work to make (at
least) one of the futures ready, right?
> I don't see why it would be more overhead with get_exception(). In fact,
> when T is an expensive-to-move type, then get_exception() allows us to test
> for success/failure without paying for that cost of moving T until we call
> get().
>
I don't like the suggestion that get_exception() would be used for "do we
have a result?" queries, because that seems backwards i.e. asking "is it
ready and did it fail?" seems like a bad alternative to "is it ready and
did it succeed?" ... I realise currently we have neither. We don't even
have "is it ready?" you can only do wait_for(0s) == future_status::ready.
But if we had a cheap "is a successful result ready?" query I think that
would be preferable to trying to retrieve the exception and using "null
exception_ptr" to mean it succeeded and there's a result. That doesn't mean
get_exception() wouldn't be useful, I just think it shows we're missing
_other_ useful members which would mean you don't need to use
get_exception() to work around the absence of those members.
> If we only have get_result(), we'd pay the move cost twice - once when
> returning the std::expected from get_result(), and once more when
> extracting from the std::expected.
>
> Ultimately, get_result() can be implemented in terms of get_exception()
> but not vice-versa, so get_exception() is the more fundamental feature.
>
Fair point.
> I am a fan of std::expected, and I do think that get_result() would be a
> great addition in its own right once std::expected supports references.
> However, get_result() can't be implemented yet, and it isn't a complete
> alternative for get_exception().
>
>
>
> On 07/06/2025 22:09, Jonathan Wakely wrote:
>
>
>
> On Sat, 7 Jun 2025, 20:26 Stewart Becker, <stewart_at_[hidden]> wrote:
>
>> Thank you, Jonathan for your insights.
>> On 07/06/2025 15:35, Jonathan Wakely wrote:
>>
>>
>>
>> On Sat, 7 Jun 2025 at 15:15, Jonathan Wakely <cxx_at_[hidden]> wrote:
>>
>>>
>>>
>> N.B. calling future::get() doesn't necessarily mean that calling it again
>> is UB. It's specified to change valid() to false, but whether that's UB is
>> up to the implementation, and recommended practice is to diagnose it by
>> throwing, so no UB.
>>
>> According to https://en.cppreference.com/w/cpp/thread/future/get,
>> calling get() when valid() is false is UB. The standard doesn't say this
>> explicitly, but it does say that get() retrieves the value/throws the
>> exception stored in the shared state, which doesn't exist when valid() is
>> false. Thus the behaviour of get() when valid() is false is literally
>> undefined. But I concede that it's not called out as UB either. Perhaps
>> this needs addressing in a separate issue?
>>
>
> It says it here:
>
> https://eel.is/c++draft/futures#unique.future-3
>
> But it also recommends that it should be defined to throw by
> implementions. So they should check for the "no state" case and throw, not
> just crash or set fire to your toes.
>
> And that's why I think that the right way to phrase the behaviour of your
> proposal is in terms of whether valid() is true or false. Because then
> paragraph 3 days what happens (and depending on the implementation, it
> might not be UB).
>
> Thank you for the pointer. You've already convinced me that defining the
> behaviour in terms of valid() is true is the way forward.
>
> Calling std::future<T>::get() sets valid() to false, even for
>> future<void>, because the future releases the shared state as a side-effect
>> of calling get(). The "result" may not be changed, but the future itself
>> is. We absolutely don't want get_exception() to invalidate the future - at
>> least when there is a good result in the shared state, as we need to be
>> able to call get() later. This is less important when there is an
>> exception rather than a value in the shared state, but for get_exception()
>> to invalidate the future in some cases but not in others would complicate
>> matters to no obvious benefit.
>>
>
> Agreed. And exception_ptr is already reference counted so if you get a
> copy of the future's exception_ptr, it remains valid even if the future
> goes away. OK, you've convinced me.
>
> Thank you.
>
>
> I'm probably just being unimaginative but I don't see why you'd want to
> stay in the "future space" and return another future when you've already
> extracted the result (or the exception) from it. Doesn't that just add more
> overhead to have to get the result (or exception) out of the future again?
>
> I suppose for the or_else case, the future you return for the exceptional
> case might not be ready yet, so allows more asynchronous work.
>
> But for the when_any case, don't you end up waiting for a result, and so
> there's no more asynchronous work? It always returns a ready future, so
> could just return the result instead. I suppose staying in the "future
> space" allows you to combine the returned future into another when_any
> call, or some other monad working with futures.
>
> Exactly. In a word, the reason for staying in future-space is composition.
>
Ack.
> Also bear in mind that futures aren't only used for asynchronous work,
> they can also be used for deferred, sychronous work.
>
Sure, but again, your when_any would execute any deferred work to make (at
least) one of the futures ready, right?
> I don't see why it would be more overhead with get_exception(). In fact,
> when T is an expensive-to-move type, then get_exception() allows us to test
> for success/failure without paying for that cost of moving T until we call
> get().
>
I don't like the suggestion that get_exception() would be used for "do we
have a result?" queries, because that seems backwards i.e. asking "is it
ready and did it fail?" seems like a bad alternative to "is it ready and
did it succeed?" ... I realise currently we have neither. We don't even
have "is it ready?" you can only do wait_for(0s) == future_status::ready.
But if we had a cheap "is a successful result ready?" query I think that
would be preferable to trying to retrieve the exception and using "null
exception_ptr" to mean it succeeded and there's a result. That doesn't mean
get_exception() wouldn't be useful, I just think it shows we're missing
_other_ useful members which would mean you don't need to use
get_exception() to work around the absence of those members.
> If we only have get_result(), we'd pay the move cost twice - once when
> returning the std::expected from get_result(), and once more when
> extracting from the std::expected.
>
> Ultimately, get_result() can be implemented in terms of get_exception()
> but not vice-versa, so get_exception() is the more fundamental feature.
>
Fair point.
> I am a fan of std::expected, and I do think that get_result() would be a
> great addition in its own right once std::expected supports references.
> However, get_result() can't be implemented yet, and it isn't a complete
> alternative for get_exception().
>
>
Received on 2025-06-08 19:22:16