C++ Logo

std-proposals

Advanced search

Re: [std-proposals] std::future::get_exception()

From: Stewart Becker <stewart_at_[hidden]>
Date: Sun, 8 Jun 2025 11:21:59 +0100
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. Also bear in mind that futures aren't only used for
asynchronous work, they can also be used for deferred, sychronous work.

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(). 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.
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 10:22:03