Date: Sat, 7 Jun 2025 20:25:48 +0100
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?
> 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.
> 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.
> 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.
>
>
> *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>?
>
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?
> 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.
> 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.
> 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.
>
>
> *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 19:25:51