Thank you for the pointer. You've already convinced me that defining the behaviour in terms of valid() is true is the way forward.
On Sat, 7 Jun 2025, 20:26 Stewart Becker, <stewart@sibecker.co.uk> 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@kayari.org> 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:
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).
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?
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 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.
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().