C++ Logo

std-proposals

Advanced search

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

From: Jonathan Wakely <cxx_at_[hidden]>
Date: Sat, 7 Jun 2025 22:09:40 +0100
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).





>
>> and so get_exception() could be a
>>> const member function of std::future.
>>>
>>
> I think what you want to say is that it wouldn't change valid() to false.
>
> Agreed. Adopting the standard's language in the specification of .get(),
> i.e. referring to valid() and "the stored exception, if an exception was
> stored in the shared state" is a better way of describing the behaviour all
> round.
>
> Although I'm not sure about that, I don't see why you should only be able
> to call future<void>::get() only once (when that doesn't modify the result,
> because there isn't one!) but call future<void>::get_exception() more than
> once. The main justification I can see is to support code like this that
> wants to check for an exception first and if it's not present, get the
> result:
>
> 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.


auto fut = prom.get_future();
> if (auto ex = fut.get_exception())
> handleError(exception_ptr_cast<E>(ex));
> else
> handleResult(fut.get());
>
>
> but if you could get expected<R,exception_ptr> out then you wouldn't need
> that anyway, you would only need to call the function to get the expected
> once, then you could test that at leisure:
>
> auto fut = prom.get_future();
> if (auto r = fut.get_res())
> handleResult(*r);
> else
> handleError(exception_ptr_cast<E>(r.error()));
>
> certainly this is a common pattern, but isn't the only one. Recall that
> one of the motiviations behind this is to be able to write or_else and
> when_any functions for futures. These should not need to not call .get(),
> even in the successful case:
>
> template<typename T, typename Function>
> std::future<T> or_else(std::future<T>&& fut, Function&& f) // f() is
> required to return a future<T> (c.f. optional::or_else)
> {
> if(auto ex = fut.get_exception())
> return std::forward<Function>(f)(ex);
> else
> return std::move(fut);
> }
>
> template<typename T>
> std::future<T> when_any(std::vector<std::future<T>>&& futures)
> {
> for(auto& future : futures) {
> if (!future.get_exception())
> return std::move(future);
> }
> return std::move(futures.back()); // Handling empty vector ignored for
> this expositional example
> }
>
> In each case, working with a std::expected or calling .get() would require
> re-wrapping the result into a future again, when the above simply use
> simply moves the future.
>

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.


This API would also give you all the monadic functions of expected.
>
> prom.get_future().get_result().and_then(handleResult).or_else(handleError);
>
> The above expression is an expected, rather than a future, so doesn't
> grant monadic operations on std::future. Maybe it's my lack of insight,
> but I don't see how to us this pattern to write when_any or or_else that
> return futures.
>

Sure, I was thinking of doing monadic operations on the result not chaining
futures. So I agree this isn't a solution for your when_any case.



>>>
>>> *Alternatives*
>>>
>>> It might be possible, within a given codebase, to achieve equivalent
>>> functionality by the use of std::future<std::expected<T, exception_ptr>>
>>> *and* careful definition of the functions used with std::future's
>>> generators std::async & std::packaged_task. However, this has two
>>> drawbacks:
>>>
>>> 1. std::expected does not support wrapping references - one has to wrap
>>> the reference somehow, leading to more complicated code. This could be
>>> mitigated by having std::expected support references. This idea was
>>> mentioned (but not proposed) in P2988r0 and anyway was subsequently
>>> dropped in later revisions of that paper. I am not aware of any other
>>> work in this area.
>>>
>>
>> I expect it will come back and we'll get expected<T&,E>, but not as part
>> of the same proposal as optional<T&>.
>>
>> If we get it for C++29 then this point is no longer relevant, since your
>> idea couldn't get accepted before C++29 anyway.
>>
> There is nothing active on this front as yet so it's a big "if" at
> present. I dare say both get_result() and get_exception() could be
> accomodated in time, but as things stand only get_expected is possible.
>
> Furthermore, get_exception() is const. That may not be all that
> important, as all almoost all other uses uses of future aren't const, but
> there is one relevant case: future<void>.
>
>
>>> 2. Regardless, it is still possible for std::future<std::expected<T,
>>> std::exception_ptr>>::get() to throw. To avoid this, care must be taken
>>> with the functions given to std::future's sources (std::asyc,
>>> std::packaged_task etc.) to ensure than the exception is supplied only
>>> via a std::unexpected<std::exception_ptr>. Furthermore, the consumer of
>>> the future must know that this is the case. Generic code using this
>>> approach would still benefit from being able to tell if the future.get()
>>> will throw or not.
>>>
>>
>>
>> Right, but I still think we could improve your program by using expected.
>>
>
> Sorry, phone autocorrect turned "proposal" into "program".
>
>
>>
>> What if instead of get() and get_exception() we had get() and
>> get_result() where the latter returns expected<R,exception_ptr>?
>>
>>
>
>

Received on 2025-06-07 21:10:00