Contributors:
Marshall Cline | Carter Edwards | Jay Feldblum |
Andrii Grynenko | Jared Hoberock | Hartmut Kaiser |
Chris Kohlhoff | Chris Mysen | Eric Niebler |
Sean Parent | Cory Perry | Felix Petriconi |
Kirk Shoop | Mathias Stearn |
1. Introduction
This paper introduces a hierarchy of concepts for future types that are designed to:
-
Interoperate With Executors: The concepts should require the functionality needed by executors.
-
Compose With Each Other: The concepts should require the types to be composable.
-
Stay Skinny: The concepts should require absolutely nothing else so that it is not burdensome to write future types.
There are five concepts introduces in this paper:
-
FutureContinuation
, invocable objects that are called with the value or exception of a future as an argument. -
SemiFuture
, which can be bound to an executor, an operation which produces aContinuableFuture
(f = sf.via(exec)
). -
ContinuableFuture
, which refinesSemiFuture
and instances can have oneFutureContinuation
attached to them (f.then(c)
), which is executed on the future’s associated executor when the future becomes ready. -
SharedFuture
, which refinesContinuableFuture
and instances can have multipleFutureContinuation
s attached to them. -
Promise
, each of which is associated with a future and make the future ready with either a value or an exception.
Or, described another way:
template <typename T> struct FutureContinuation { // At least one of these two overloads exists: auto operator()(T value); auto operator()(exception_arg_t, exception_ptr exception); }; template <typename T> struct SemiFuture { template <typename Executor> ContinuableFuture<Executor, T> via(Executor&& exec) &&; }; template <typename Executor, typename T> struct ContinuableFuture { template <typename RExecutor> ContinuableFuture<RExecutor, T> via(RExecutor&& exec) &&; template <typename Continuation> ContinuableFuture<Executor, auto> then(Continuation&& c) &&; }; template <typename Executor, typename T> struct SharedFuture { template <typename RExecutor> ContinuableFuture<RExecutor, auto> via(RExecutor&& exec); template <typename Continuation> SharedFuture<Executor, auto> then(Continuation&& c); }; template <typename T> struct Promise { void set_value(T value) &&; template <typename Error> void set_exception(Error exception) &&; bool valid() const; };
In the following sections, we describe some of the key aspects of our proposed design.
1.1. The Future/Promise Execution Model
In the Concurrency TS v1, it is unspecified where a .then
continuation will be run.
There are a number of possible answers:
-
Consumer Side: The consumer execution agent always executes the continuation.
.then
blocks until the producer execution agent signals readiness. -
Producer Side: The producer execution agent always executes the continuation.
.set_value
blocks until the consumer execution agent signals readiness. -
inline_executor
Semantics: If the shared state is ready when the continuation is set, the consumer thread executes the continuation. If the shared state is not ready when the continuation is set, the producer thread executes the continuation. -
thread_executor
Semantics: A newstd::thread
executes the continuation.
This is a source of trouble ([P0701r0] and [P0701r1]). The first two answers are undesirable, as they would require blocking, which is not ideal for an asynchronous interface. The third and fourth are likewise distasteful, as they can be vague or inefficient (respectively).
Executors, finally, give us at least a partial solution to this problem.
The question changes to "where do we enqueue work into the executor"?
The answer: work is always enqueued on the consumer side as if, but not
necessarily, via then_execute
.
You can query executor properties to determine whether or not the executor’s
APIs will block, which tells you whether or not continuation attachment (consumer side) or future fulfillment (producer side) potentially blocks
pending execution (e.g. inline_executor
semantics).
1.2. Interactions with Executors
The executors proposal defines a collection of executor types intended for use managing the execution of tasks on resources. There are three fundamental executor categories that cover directionality and control of launch:
-
one-way
-
two-way
-
then
The first two could be considered immediately launched. That is that once handed to the executor, they may start immediately, assuming the internal executor policies and resources allow it. This makes them very useful for lazy-launch scenarios.
Lazy launch scenarios are common in callback-based code, and in a wide range of future library implementations such as HPX and folly. In these designs, a callback is executed on completion of some asynchronous work, and that callback enqueues work into the executor. This means that work is enqueued only after all dependencies are satisfied.
Then-executors, on the other hand, are intended for explicitly deferred work. Work can be handed to the executor dependent on prior work, before that prior work is completed. This design is fundamentally different, but offers scope for optimization by the executor of chains of dependencies that it can batch, without running additional code on completion of each.
The current executor design is intentionally generic - it makes few requirements
on the future types it can use as input dependencies for the then_execute
and bulk_then_execute
operations.
We can assume that for a future returned by a previous call to then_execute
or bulk_then_execute
, the executor understands the implementation of the
future can can perform whatever dependence tracking and optimization necessary.
This then is an implementation detail.
However, there will also be interactions where a task to run on one executor is dependent on one produced by another. For this to be practical, we need a standardised mechanism to tie the two executors together. This amounts to a standard API for triggering deferred work.
To solve this we provide two things:
-
A promise concept, that allows setting of value and/or exception.
-
A mechanism to retrieve from an executor a pair of a promise and a future, such that the future is a valid input dependence for a call to
then_execute
orbulk_then_execute
.
The promise is a write-only concept. This simplifies the definition and improves flexibility.
The future is not a full future in the sense of future concepts. It is merely a token that completes when the promise is satisfied. This means that it is useful only for connecting to then_execute
or bulk_then_execute
on the executor that provided the result.
1.3. The Exception Handling Model
Our proposal moves away from future continuations that take futures as an argument, the design used in the Concurrency TS v1. Instead, continuations take the value type of the future. This, however, requires a new exception handling model. Both the executors proposal and this paper adopt the same model, where users can provide callable objects with both a value type and an optional tag disambiguated exception handling overload.
In the example below, let f
and g
be ContinuableFuture
s and let c
be a FutureContinuation
.
g = f.then(c);
When f
is fulfilled:
-
If
f
has a valueval
: -
If
c(val)
is well-formed, it is called and the futureg
is fulfilled with the result or any exception thrown from the evaluation. -
Otherwise, the future
g
is fulfilled with the valueval
(this is known as future value propagation - see [P0701r0] and [P0701r1]). -
If
f
fails with exceptionexc
: -
If
c(exception_arg, exc)
is well-formed, it is called and the futureg
is fulfilled with the result or any exception thrown from the evaluation. -
Otherwise, the future
g
is fulfilled with the exceptionexc
(this is known as future exception propagation - see [P0701r0] and [P0701r1]).
Note that if both overloads are defined, decltype(G(exception_arg, err))
shall be convertible to decltype(G(val))
.
This paper defines some helper types which can be used to build callable
arguments that meet the FutureContinuation
arguments.
For example, on_value_or_error(f, g)
takes two callables (a on-value
continuation and on-error continuation) and returns a single FutureContinuation
object composed from the two.
1.4. The Future Cancellation Model
This paper proposes a way to hook a cancellation notifier into a future/promise pair, using cancellable_promise_contract_t
.
It allows for authors of future types to support cancellation if they want to, and to correctly hook into the cancellation mechanisms
of other futures they interoperate with - such as inside .via
, where the implementation can create a future/promise pair that will,
on cancellation, invoke a function that will cancel the current future.
A sample implementation of .via
could be as follows:
template <typename Executor> auto via(Executor && ex) && { auto cancel = [this] { handle_cancellation(); }; auto [promise, future] = execution::query(ex, cancellable_promise_contract_t{ cancel }); then([promise = std::move(promise)](auto value) { promise.set_value(std::move(value)); }); return future; }
We are currently not proposing a concrete cancellation API; we plan to do that in a future revision of the paper. At this time, there is no guarantee that a future honors a cancellation notifier, and no requirements as to when it actually invokes it.
1.5. SemiFuture
and .via
For Composition
The SemiFuture
concept and .via
mechanism ([P0783r0] and [P0904r0]) gives
us control of the transfer of execution ownership between executors and a way
to convert between different future types.
One problem that arises in the executor model is how a future-returning interface can dictate the executor that callers attach continuations to.
SemiFuture
and .via
is mechanism that allows the caller to control the
executor that subsequent chaining operations use.
An interface may return a handle to its work, a future in the most abstract sense,
that does not provide a means to chain more work.
This future will complete on whatever executor that interface was using.
This returned future satisfies the SemiFuture
concept, and the caller is hence
aware that to make use of it they must attach an executor, using .via
, to
transition from the interface’s executor to the caller’s executor.
From that point on, the caller can use the future as necessary, safely enqueuing
work onto a known executor, and protecting the interface from misuse.
Additionally, since ContinuableFuture
and SharedFuture
s are also SemiFuture
s, .via
provides a way to convert (if possible) from one future type
(associated with a particular executor type) to another "foreign" future type
(associated with another executor type).
As a simple example:
std::execution::semi_future<DataType> async_api() { std::execution::continuable_future<APIExecutor, SerializedDataType> = doAsyncWork(); return std::move(f).then( [](SerializedDataType&& val){ return deserialize(val); }); } void caller() { LocalExecutor e; auto sf = async_api(); auto f = std::move(sf).via(e); std::move(f).then([](DataType&& data) { std::cout << "Name: " << data.name() << "\n"; }); }
There is a strict separation of control here between caller
and async_api
.
1.6. Consuming Interfaces Are Rvalue Reference Qualified
In a change to the model used in std::future
, where std::future::get()
is a
consuming operation but is l-value qualified, consuming operations in this
proposal are r-value qualified.
For free functions this should be obvious. If we had a free function future_then
that takes a future and returns a value, we will likely r-value
qualify it:
std::future<T2> future_then(std::future<T>&& f, continuation&& c);
For consistent and safe use, the same should apply to the equivalent builtin methods.
In chaining code, this falls out cleanly:
auto f2 = do_async_thing().then([](T val){return val;}).then([](T2 val){return val;});
Occasionally we must be explicit:
auto f = do_async_thing(); auto f2 = std::move(f).then([](T val){return val;}).then([](T2 val){return val;});
but this is a minor inconvenience given that it allows the tooling to warn on use-after-move and related misuses.
1.7. Blocking Interfaces (.get
and .wait
)
The future concepts in this paper are intended to express the minimal requirements for executors interoperation and composition.
Executors do not require a blocking interface, and futures do not require a blocking interface to compose.
This paper proposes the addition of the free functions std::this_thread::future_wait
and std::this_thread::future_get
.
These functions will block on any SemiFuture
type, and are suitable for use within std::thread
execution agents.
Some individuals may feel that blocking interfaces are more fundamental for futures than continuation chaining interfaces - some may desire to not provide continuation chaining interfaces at all.
We believe that this is a perfectly valid design; however, such non-continuable futures are outside of the scope of this work.
-
If you want to write a future that only has blocking interfaces and doesn’t support continuations, that’s fine! But that type of future cannot interoperate with executors.
-
If you want to write a future that only doesn’t have blocking interfaces and does support continuations, that’s fine! That type of future can interoperate with executors, so you should conform to the proposed concepts.
-
If you want to write a future that has blocking interfaces and supports continuations, that’s fine! That type of future can interoperate with executors, so you should conform to the proposed concepts.
[P0701r0] and [P0701r1] discuss some of the challenges relating to future/promise synchronization that motivated the current design.
1.8. Future Work
There is plenty of future work on futures to be done. Here is a list of topics that are currently on our agenda:
-
Forward progress guarantees for futures and promises.
-
Requirements on synchronization for use of futures and promises from non-concurrent execution agents.
-
std::future
/std::promise
interoperability. -
Future unwrapping, both
future<future<T>>
and more advanced forms. -
when_all
/when_any
/when_n
. -
async
.
1.9. Prior Work
This work is unification of prior papers written individually by the authors:
2. Proposed New Wording
The following wording is purely additive to the executors proposal.
2.1. The Future/Promise Execution Model
-
A future is an object that executes, on a executor, continuations that are passed an asynchronous result as a parameter.
-
A uniquely owned future moves from the asynchronous result and can have at most one continuation attached.
-
A non-uniquely owned future copies from the asynchronous result and can have one or more continuations attached.
-
A promise is an object that produces an asynchronous result and is associated with a future. [ Note: Although a promise is associated with one future, multiple shared future objects may refer to the same underlying shared future. - end note ]
-
The status of a future:
-
Is either ready, not ready, or invalid.
-
Shall be ready if and only if the future holds a value or an exception ready for retrieval.
-
An asynchronous result is either a value or an exception. The result is created by the promise and accessed by the future.
-
A future may optionally have a cancellation notifier, which is a nullary callable object.
-
When a future is cancelled:
-
If the future has a cancellation notifier, it is called in the calling thread.
-
When a continuation is attached to a future:
-
Then,
-
If the status is ready or not ready:
-
The continuation is enqueued for execution pending readiness of future on the future’s executor, with the asynchronous result as its parameter.
-
Finally, if the future is a uniquely owned future, the status is set to invalid.
-
-
-
When a promise fulfills its associated future with a value or error, if the lifetime of the future has not ended:
-
Then,
If the lifetime of the future has ended, fulfillment has no effect.
-
Fulfillment of a future synchronizes with execution of continuations that were enqueued for execution by that future.
-
The lifetime of the continuations and the asynchronous result passed to the continuations shall last until execution of the continuations completes. [ Note: The future may move (if it is a uniquely owned future) or copy (if it is a non-uniquely owned future) from the result into a new object to ensure this behavior. - End Note ] [ Note: The future may move from continuations into new object to ensure this behavior. - End Note ]
-
The lifetime of the continuations and the asynchronous result passed to the continuations shall last until execution of the continuations completes. [ Note: The promise may move or copy (if the result is
CopyConstructible
) from the result into a new object to ensure this behavior. - End Note ] [ Note: The promise may move from continuations into new object to ensure this behavior. - End Note ] -
Upon destruction of a promise:
-
Setting the status of a future synchronizes with operations that check the future's status.
-
Operations that modify the set of continuations stored in a future synchronize with each other.
-
Successful fulfillment of a future synchronizes with attachment of continuations to that future.
-
Successful attachment of a continuation to a future synchronizes with fulfillment of the promise associated with that future.
-
If a future has a cancellation notifier, successful fulfillment of it’s associated promise synchronizes with cancellation of the future.
2.2. FutureContinuation
Requirements
namespace std::execution { struct exception_arg_t { explicit exception_arg_t() = default; }; inline constexpr exception_arg_t exception_arg{}; template <typename F, typename T> struct is_future_value_continuation; template <typename F, typename T> inline constexpr bool is_future_value_continuation_v = is_future_value_continuation<F, T>::value; template <typename F> struct is_future_exception_continuation; template <typename F> inline constexpr bool is_future_exception_continuation_v = is_future_exception_continuation<F>::value; template <typename F, typename T> struct is_future_continuation; template <typename F, typename T> inline constexpr bool is_future_continuation_v = is_future_continuation<F, T>::value; }
-
A future continuation is a callable object that consumes the value of a future.
-
The struct
exception_arg_t
is an empty structure type used as a unique type to disambiguateFutureContinuation
overloads that are called when a future holds an exception fromFutureContinuation
overloads that are called when a future holds a value. -
This sub-clause contains templates that may be used to determine at compile time whether a type meets the requirements of future continuations for a particular future value type. Each of these templates is a
BinaryTypeTrait
with a base characteristic oftrue_type
if the corresponding condition is true, andfalse_type
otherwise. -
A
FutureContinuation
type shall meet theMoveConstructible
requirements and the requirements described in the Tables below.
Type Property Queries
Template | Condition | Preconditions |
---|---|---|
template <typename F, typename T> struct is_future_value_continuation; | is_move_constructible_v<F> && is_invocable_v<F, T>
| T is a complete type.
|
template <typename F> struct is_future_exception_continuation; | is_move_constructible_v<F> && is_invocable_v<F, exception_arg_t, exception_ptr>
| T is a complete type.
|
template <typename F, typename T> struct is_future_continuation; | is_future_value_continuation_v<F, T> || is_future_exception_continuation_v<F>
| T is a complete type.
|
2.3. Promise
Requirements
#. A Promise
type for value type T
and error type E
shall meet the MoveConstructible
requirements, the MoveAssignable
requirements, and the requirements described in the Tables below.
Descriptive Variable Definitions
Variable | Definition |
---|---|
T
|
Either:
|
t | a value of a type contextually convertible to T
|
e
| a value of type contextually convertible to exception_ptr
|
P<T>
| A promise type for value type T
|
p
| An rvalue of type P<T>
|
Expression | Return Type | Operational semantics |
---|---|---|
promise_value_t<P<T>>
| T
| |
p.set_value(t)
| void
|
|
p.set_value()
| void
|
|
p.set_exception(e)
| void
| Completes the promise and associated future with the error e .
|
p.valid()
| Contextually convertible to bool .
| true if the promise has an associated future that is incomplete, false otherwise.
|
2.3.1. Promise Contract Executor Properties
template <class T> struct promise_contract_t { static constexpr bool is_requirable = false; static constexpr bool is_preferable = false; using polymorphic_query_result_type = std::pair<std::execution::promise<T>, std::execution::semi_future<T>>; }; template <typename T> inline constexpr promise_contract = promise_contract_t<T>{};
The promise_contract_t
property can be used only with query.
The result of a query of the promise_contract_t
property applied to a ThenExecutor
or BulkThenExecutor
is a std::pair
consisting of a Promise
and an implementation-defined token type that will be interpreted as a valid
input future by calls to then_execute
or bulk_then_execute
and that is
satisfied by calling set_value
or set_exception
on the promise.
The value returned from execution::query(e, promise_contract_t<T>)
, where e
is an executor and T
is a type, should be unique for any given call.
When e
is a ThenExecutor
or BulkThenExecutor
the result of the query is a std::pair
where first value is an instance of a type matching the Promise
requirements and the second is a token type that e
will interpret as a valid
future parameter to calls to then_execute
or bulk_then_execute
.
template <T, C> struct cancellable_promise_contract_t { static constexpr bool is_requirable = false; static constexpr bool is_preferable = false; using polymorphic_query_result_type = std::pair<std::execution::promise<T>, std::execution::semi_future<T>>; template<class Executor> static constexpr decltype(auto) static_query_v = Executor::query(promise_contract_t()); template<class Executor> friend std::pair<std::execution::promise<T>, std::execution::semi_future<T>> query(const Executor& ex, const cancellable_promise_contract_t&); CancellationNotifier cancellation_notifier; };
The cancellable_promise_contract_t
property can be used only with query. cancellable_promise_contract_t
differs from promise_contract_t
in that the
query carries a cancellation callback, cancellation_notifier
, that will be
called as std::invoke(cancellation_notifier)
by the ThenExecutor
on
cancellation of the task dependent on the future resulting from the cancellable_promise_contract_t
query.
The result of a query of the cancellable_promise_contract_t
property applied
to a ThenExecutor
or BulkThenExecutor
is a std::pair
consisting of a Promise
and an implementation-defined token type that will be interpreted as
a valid input future by calls to then_execute
or bulk_then_execute
, that
is satisfied by calling set_value
or set_exception
on the promise and that
supports cancellation by the executor.
The value returned from execution::query(e, cancellable_promise_contract_t<T>{cancellation_notifier})
,
where e
is an executor and T
is a type, should be unique for any given call.
When e
is a ThenExecutor
or BulkThenExecutor
the result of the query is a std::pair
where first value is an instance of a type matching the Promise
requirements and the second is a token type that e
will interpret as a valid
future parameter to calls to then_execute
or bulk_then_execute
.
template<class Executor> friend std::pair<std::execution::promise<T>, std::execution::semi_future<T>> query(const Executor& ex, const cancellable_promise_contract_t&);
-
Returns: A
std::pair
where the first value is astd::execution::promise<T>
and the second is astd::execution::semi_future<T>
such that the future was retrieved from the promise. -
Remarks: This function shall not participate in overload resolution unless
executor_future_t<Executor, T>
isstd::execution::semi_future<T>
.
2.4. SemiFuture
Requirements
-
A semi future is an object that can be bound to an executor to produce a future.
-
A
SemiFuture
type for typeT
shall meet theMoveConstructible
requirements, theMoveAssignable
requirements, and the requirements described in the Tables below.
Descriptive Variable Definitions
Variable | Definition |
---|---|
T
|
Either:
|
SF<T>
| A SemiFuture type for value type T .
|
sf
| An rvalue of type SF<T> .
|
E
|
An executor type, either:
|
e
| A value of type E .
|
CF<T, E>
|
A ContinuableFuture type for value type T and executor type E , either:
|
SemiFuture
Requirements
Expression | Return Type | Operational Semantics |
---|---|---|
future_value_t<SF<T>>
| T
| |
future_exception_t<SF<T>>
| Implicitly convertible to exception_ptr .
| |
sf.via(e)
| implementation-defined |
Returns: A ContinuableFuture for value type T that is bound to the executor e and will be made ready with the value or exception of sf when sf is made ready.
Throws: If |
2.5. ContinuableFuture
Requirements
-
A continuable future is a future that is bound to an executor and can have continuations attached to it.
-
A
ContinuableFuture
shall meet theSemiFuture
requirements and the requirements described in the Tables below.
Descriptive Variable Definitions
Variable | Definition |
---|---|
E
| An executor type. |
e
| A value of type E .
|
T
|
Either:
|
CF<T, E>
| A ContinuableFuture type for value type T and executor type E .
|
cf
| A value of type CF<T, E> .
|
rcf
| An rvalue of type CF<T, E> .
|
val
| The value contained within the successfully completed future rcf .
|
ex
| The exception contained within the exceptionally completed future rcf .
|
G
| Any type such that is_future_continuation_v<G, T> == true .
|
g
| An object of type G .
|
R
|
Either:
|
SF<T>
| A SemiFuture type for value type T .
|
NORMAL
|
Either:
|
EXCEPTIONAL
| The expression DECAY_COPY(std::forward<G>(g))(exception_arg, std::move(ex)) .
|
ContinuableFuture
Requirements
Expression | Return Type | Operational Semantics |
---|---|---|
cf.get_executor()
| E
| Returns: The executor that the future is bound to. |
rcf.then(g)
| CF<E, R>
|
Returns: A ContinuableFuture that is bound to the executor e and
that wraps the type returned by execution of either the value or exception
operations implemented in the continuation.
Effects: When Otherwise, when If If If neither May block pending completion of The invocation of Fulfills the Synchronization: The destruction of the continuation that generates |
2.6. SharedFuture
Requirements
-
A shared future is a non-uniquely owned future that is copyable, is bound to an executor and that allows one or more continuation to be attached to it.
-
A
SharedFuture
shall meet theContinuableFuture
requirements, theCopyConstructible
requirements, theCopyAssignable
requirements and the requirements described in the Tables below.
Descriptive Variable Definitions
Variable | Definition |
---|---|
E
| An executor type. |
e
| A value of type E .
|
T
| Any (possibly cv-qualified) object type that is not an array. |
CF<T, E>
| A ContinuableFuture type for executor type E and value type T .
|
SHF<E, T>
| A SharedFuture type for executor type E and value type T .
|
shf
| A value of type SHF<E, T> .
|
NORMAL
| The expression DECAY_COPY(std::forward<G>(g))(val) if T is non-void and DECAY_COPY(std::forward<G>(g))() if T is void.
|
EXCEPTIONAL
| The expression DECAY_COPY(std::forward<G>(g))(exception_arg, ex) ,
|
SharedFuture
Requirements
Expression | Return Type | Operational Semantics |
---|---|---|
shf.then(g)
|
If T is non-void and INVOKE(declval<G>(), declval<T>()) or if T is void and INVOKE(declval<G>()) is well-formed:
Otherwise:
|
Returns: A ContinuableFuture that is bound to the executor e and
that wraps the type returned by execution of either the value or exception
operations implemented in the continuation.
Effects: When Otherwise, when If If If neither May block pending completion of The invocation of Fulfills the Postconditions: Has no observable affect on |
shf.via(e)
| Implementation-defined |
Returns: A ContinuableFuture for type T that is bound to the executor e .
Effect: Returns an implementation-defined Success: Succeeds if:
Fails at compile-time otherwise. Postconditions: Has no observable affect on |
2.7. std::execution::promise
template <class T> class promise { public: using value_type = T; promise() noexcept; promise(promise&&) noexcept; template <typename Promise> explicit promise(Promise&& p); void set_value(/* see below */) &&; template <typename Error> void set_exception(Error&& err) &&; bool valid() const noexcept; explicit operator bool() const noexcept; };
A promise
refers to a promise and is associated with a future, either through type-erasure or through construction of an underlying promise with an overload of make_promise_contract()
.
promise() noexcept;
-
Effects: Constructs a
promise
object that does not refer to a promise -
Postconditions:
-
valid() == false
.
-
promise(promise&& rhs) noexcept;
-
Effects: Move constructs a
promise
object fromrhs
that refers to the same promise and is associated with the same future (ifrhs
refers to a promise). -
Postconditions:
-
valid()
returns the same value asrhs.valid()
prior to the constructor invocation. -
rhs.valid() == false
.
-
template <class Promise> promise(Promise&& rhs) noexcept;
-
Requires:
-
Promise
meets the requirements forPromise<value_type>
-
!is_same_v<decay_t<Promise>, promise>
-
!is_reference_v<Promise>
Note: This constrains the parameter to be an rvalue reference rather than a forwarding reference —end note] -
Effects: Constructs a
promise
object that refers to the promiserhs
and is associated with the same future asrhs
. (ifrhs
is associated with a future). -
Postconditions:
-
valid()
returns the same value asrhs.valid()
prior to the constructor invocation.
-
void promise::set_value(const T& val) &&; void promise::set_value(T&& val) &&; void promise<void>::set_value() &&;
-
Effects:
-
If
valid() == true
,-
For
promise::set_value(const T& val) &&
: equivalent to callingset_value(val)
on the promise that*this
refers to. -
For
promise::set_value(T&& val) &&
: equivalent to callingset_value(std::move(val))
on the promise that*this
refers to. -
For
promise<void>::set_value() &&
: equivalent to callingset_value()
on the promise that*this
refers to.
-
-
Otherwise, throws
-
Throws:
future_error
with error conditionno_state
ifvalid() == false
-
Postconditions:
-
valid() == false
-
Notes:
promise::set_value(const T& val) &&
does not participate in overload resolution unlessis_copy_constructible_v<decay_t<T>>
template <typename Error> void set_exception(Error&& err) &&;
-
Requires:
-
Error
is contextually convertible toexception_ptr
-
Effects:
-
If
valid() == true
, equivalent to callingset_exception(exception_ptr{forward<Error>(err)})
on the promise that*this
refers to. -
Otherwise, throws
-
Throws:
future_error
with error conditionno_state
ifvalid() == false
-
Postconditions:
-
valid() == false
bool valid() const noexcept; explicit operator bool() const noexcept;
-
Returns:
true
if*this
refers to a promise and the referenced promise isvalid()
, orfalse
otherwise
2.8. std::execution::semi_future
template<class T> class semi_future { public: using value_type = T; semi_future(semi_future&&) = default; semi_future(const semi_future&) = delete; semi_future& operator=(const semi_future&) = delete; semi_future& operator=(semi_future&&) = default; template<class E> explicit semi_future(continuable_future<T, E>&&); template<class E> explicit semi_future(shared_future<T, E>&&); template<class EI> continuable_future<T, EI> via(EI) &&; };
continuable_future(semi_future&& rhs);
-
Effects: Move constructs a future object from
rhs
that refers to the same future and its associated with the same promise (ifrhs
refers to a future). -
Postconditions:
-
valid() returns the same value as rhs.valid() prior to the constructor invocation.
-
rhs.valid() == false.
-
semi_future(const semi_future& rhs);
-
Effects: Copy constructs a future object from
rhs
that refers to the same future and its associated with the same promise (ifrhs
refers to a future). -
Postconditions: valid() returns the same value as rhs.valid() prior to the constructor invocation. The validity of rhs does not change.
template<class E> semi_future(continuable_future<T, E>&& rhs);
-
Effects: Move constructs a future object from
rhs
that refers to the same future and its associated with the same promise (ifrhs
refers to a future). -
Postconditions:
-
valid() returns the same value as rhs.valid() prior to the constructor invocation.
-
rhs.valid() == false.
template<class E> explicit semi_future(shared_future<T, E>&& rhs); template<class E> explicit semi_future(const shared_future<T, E>& rhs);
-
Effects: Move constructs a future object from
rhs
that refers to the same future and its associated with the same promise (ifrhs
refers to a future). -
Postconditions:
-
valid() returns the same value as rhs.valid() prior to the constructor invocation.
-
rhs.valid() == false.
-
continuable_future<T, EI> via(EI ex) &&;
-
Effects: Returns a new future that will complete when this completes but on which continuations will be enqueued to a new executor.
-
Returns: A continuable_future modified to carry executor ex of type EI.
-
Requires:
-
e
is aThenExecutor
wheremake_promise_contract(e)
is well-formed. -
e
is aOnewayExecutor
or is convertible to a OnewayExecutor. -
Postconditions: valid() == false.
bool valid() const noexcept;
-
Returns: Returns true if this is a valid future. False otherwise.
2.9. std::execution::continuable_future
template<class T, class E> class continuable_future { public: using value_type = T; using executor_type = Ex; using semi_future_type = semi_future<T>; continuable_future(const continuable_future&) = delete; continuable_future(continuable_future&&) = default; continuable_future& operator=(const continuable_future&) = delete; continuable_future& operator=(continuable_future&&) = default; template<class E> explicit continuable_future(shared_future<T, E>&&); template<class E> explicit continuable_future(const shared_future<T, E>&); template<class ReturnFuture, class F> ReturnFuture then(FutureContinuation&&) &&; template<class EI> continuable_future<T, EI> via(EI) &&; E get_executor() const; semi_future<T> semi() &&; shared_future<T, E> share() &&; bool valid() const } };
continuable_future(continuable_future&& rhs);
-
Effects: Move constructs a future object from
rhs
that refers to the same future and its associated with the same promise (ifrhs
refers to a future). -
Postconditions:
-
valid() returns the same value as rhs.valid() prior to the constructor invocation.
-
rhs.valid() == false.
-
continuable_future(const continuable_future& rhs);
-
Effects: Copy constructs a future object from
rhs
that refers to the same future and its associated with the same promise (ifrhs
refers to a future). -
Postconditions: valid() returns the same value as rhs.valid() prior to the constructor invocation. The validity of rhs does not change.
template<class E> explicit continuable_future(shared_future<T, E>&& rhs);
-
Effects: Move constructs a future object from
rhs
that refers to the same future and its associated with the same promise (ifrhs
refers to a future). -
Postconditions:
-
valid() returns the same value as rhs.valid() prior to the constructor invocation.
-
rhs.valid() == false.
-
template<class E> explicit continuable_future(const shared_future<T, E>& rhs);
-
Effects: Copy constructs a future object from
rhs
that refers to the same future and its associated with the same promise (ifrhs
refers to a future). -
Postconditions: valid() returns the same value as rhs.valid() prior to the constructor invocation. The validity of rhs does not change.
template<class ReturnFuture, class F> ReturnFuture then(F&&) &&;
-
Requires: F satisfies the requirements of FutureContinuation.
-
Returns: A
ContinuableFuture
that is bound to the executore
and that wraps the type returned by execution of either the value or exception operations implemented in the continuation. The type of the returnedContinuableFuture
is defined byExecutor
E
. -
Effects:
-
For
NORMAL
defined as the expressionDECAY_COPY(std::forward<F>(g))(val)
ifT
is non-void andDECAY_COPY(std::forward<F>(g))()
ifT
is void. -
For
EXCEPTIONAL
defined as the expressionDECAY_COPY(std::forward<F>(f))(exception_arg, ex)
, -
When
*this
becomes nonexceptionally ready, and ifNORMAL
is a well-formed expression, creates an execution agent which invokesNORMAL
at most once, with the call toDECAY_COPY
being evaluated in the thread that called.then
. -
Otherwise, when
*this
becomes exceptionally ready, ifEXCEPTIONAL
is a well-formed expression, creates an execution agent which invokesEXCEPTIONAL
at most once, with the call toDECAY_COPY
being evaluated in the thread that called.then
. -
If
NORMAL
andEXCEPTIONAL
are both well-formed expressions,decltype(EXCEPTIONAL)
shall be convertible toR
. -
If
NORMAL
is not a well-formed expression andEXCEPTIONAL
is a well-formed expression,decltype(EXCEPTIONAL)
shall be convertible todecltype(val)
. -
If neither
NORMAL
norEXCEPTIONAL
are well-formed expressions, the invocation of.then
shall be ill-formed. -
May block pending completion of
NORMAL
orEXCEPTIONAL
. -
The invocation of
.then
synchronizes with (C++Std [intro.multithread]) the invocation off
. -
Stores the result of either the
NORMAL
orEXCEPTIONAL
expression, or any exception thrown by either, in the associated shared state of the resultingContinuableFuture
. Otherwise, stores eitherval
ore
in the associated shared state of the resultingContinuableFuture
.
-
-
Postconditions: valid() == false.
continuable_future<T, EI> via(EI ex) &&;
-
Effects: Returns a new future that will complete when this completes but on which continuations will be enqueued to a new executor.
-
Returns: A continuable_future modified to carry executor ex of type EI.
-
Requires:
-
e
is aThenExecutor
wheremake_promise_contract(e)
is well-formed. -
e
is aOnewayExecutor
or is convertible to a OnewayExecutor. -
Postconditions: valid() == false.
E get_executor() const;
-
Returns: If is_valid() returns true returns the executor contained within the future. Otherwise throws std::future_error.
semi_future<T> semi() &&;
-
Returns: Returns a semi_future of the same value type as *this and that completes when this completes, but that erases the executor. is_valid() on the returned semi_future will return the same value as is_valid() on *this.
bool valid() const noexcept;
-
Returns: Returns true if this is a valid future. False otherwise.
shared_future<T, E> share() &&;
-
Returns: shared_future
(std::move(*this)). -
Postconditions: valid() == false.
2.10. std::execution::shared_future
namespace std::execution { template<class T, class E> class shared_future { public: using value_type = T; using executor_type = Ex; using semi_future_type = semi_future<T>; shared_future(const shared_future&) = default; shared_future(shared_future&&) = default; shared_future& operator=(const shared_future&) = default shared_future& operator=(shared_future&&) = default; template<class E> explicit shared_future(continuable_future<T, E>&&); template<class ReturnFuture, class F> ReturnFuture then(FutureContinuation&&); template<class EI> shared_future<T, EI> via(EI); E get_executor() const; semi_future<T> semi(); bool valid() const; } }; }
shared_future(shared_future&& rhs);
-
Effects: Move constructs a future object that refers to the shared state that was originally referred to by rhs (if any).
-
Postconditions:
-
valid() returns the same value as rhs.valid() prior to the constructor invocation.
-
rhs.valid() == false.
-
shared_future(const shared_future& rhs);
-
Effects: Copy constructs a future object that refers to the shared state that was originally referred to by rhs (if any).
-
Postconditions: valid() returns the same value as rhs.valid() prior to the constructor invocation. The validity of rhs does not change.
explicit shared_future(continuable_future&& rhs);
-
Effects: Move constructs a future object that refers to the shared state that was originally referred to by rhs (if any).
-
Postconditions:
-
valid() returns the same value as rhs.valid() prior to the constructor invocation.
-
rhs.valid() == false.
-
template<class ReturnFuture, class F> ReturnFuture then(F&&);
-
Requires: F satisfies the requirements of FutureContinuation.
-
Returns: A
ContinuableFuture
that is bound to the executore
and that wraps the type returned by execution of either the value or exception operations implemented in the continuation. The type of the returnedContinuableFuture
is defined byExecutor
E
. -
Effects:
-
For
NORMAL
defined as the expressionDECAY_COPY(std::forward<F>(g))(val)
ifT
is non-void andDECAY_COPY(std::forward<F>(g))()
ifT
is void. -
For
EXCEPTIONAL
defined as the expressionDECAY_COPY(std::forward<F>(f))(exception_arg, ex)
, -
When
*this
becomes nonexceptionally ready, and ifNORMAL
is a well-formed expression, creates an execution agent which invokesNORMAL
at most once, with the call toDECAY_COPY
being evaluated in the thread that called.then
. -
Otherwise, when
*this
becomes exceptionally ready, ifEXCEPTIONAL
is a well-formed expression, creates an execution agent which invokesEXCEPTIONAL
at most once, with the call toDECAY_COPY
being evaluated in the thread that called.then
. -
If
NORMAL
andEXCEPTIONAL
are both well-formed expressions,decltype(EXCEPTIONAL)
shall be convertible toR
. -
If
NORMAL
is not a well-formed expression andEXCEPTIONAL
is a well-formed expression,decltype(EXCEPTIONAL)
shall be convertible todecltype(val)
. -
If neither
NORMAL
norEXCEPTIONAL
are well-formed expressions, the invocation of.then
shall be ill-formed. -
May block pending completion of
NORMAL
orEXCEPTIONAL
. -
The invocation of
.then
synchronizes with (C++Std [intro.multithread]) the invocation off
. -
Stores the result of either the
NORMAL
orEXCEPTIONAL
expression, or any exception thrown by either, in the associated shared state of the resultingContinuableFuture
. Otherwise, stores eitherval
ore
in the associated shared state of the resultingContinuableFuture
.
-
-
Postconditions: No observable change to *this.
shared_future<T, EI> via(EI ex);
-
Effects: Returns a new future that will complete when this completes but on which continuations will be enqueued to a new executor.
-
Returns: A shared_future modified to carry executor ex of type EI.
-
Requires:
-
e
is aThenExecutor
wheremake_promise_contract(e)
is well-formed. -
e
is aOnewayExecutor
or is convertible to a OnewayExecutor. -
Postconditions: No observable change to *this.
E get_executor() const;
-
Returns: If is_valid() returns true returns the executor contained within the future. Otherwise throws std::future_error.
semi_future<T> semi();
-
Returns: Returns a semi_future of the same value type as *this and that completes when this completes, but that erases the executor. is_valid() on the returned semi_future will return the same value as is_valid() on *this.
bool valid() const noexcept;
-
Returns: Returns true if this is a valid future. False otherwise.
2.11. std::execution::make_promise_contract
template <class T, class Executor> /* see below */ make_promise_contract(const Executor& ex) requires execution::is_then_executor_v<Executor> && execution::can_query_v<Executor, promise_contract_t<T>>
-
Effects: equivalent to
execution::query(ex, promise_contract_t<T>{})
-
Returns: same as
execution::query(ex, promise_contract_t<T>{})
template <class T, class Executor> pair<promise<T>, continuable_future<T, decay_t<Executor>> make_promise_contract(const Executor& ex) requires execution::is_one_way_executor_v<Executor>
-
Returns: A pair of:
template <class T> pair<promise<T>, semi_future<T>> make_promise_contract()
-
Returns: A pair of:
2.12. Generic Future Blocking Functions
In [thread.syn] and [thread.thread.this] add:
namespace std::this_thread { template<class Future> void future_wait(Future& f) noexcept; template<class Future> future_value_t<decay_t<Future>> future_get(Future&& f); }
In [thread.thread.this] add:
template<class Future> void future_wait(Future& f) noexcept;
-
Requires:
Future
meets the requirements ofSemiFuture
. -
Effects: Blocks the calling thread until
f
becomes ready. -
Synchronization: The destruction of the continuation that fulfills
f
's synchronizes withfuture_wait
calls blocking untilf
becomes ready.template<class Future> future_value_t<decay_t<Future>> future_get(Future&& f);
-
Requires:
-
decay_t<Future>
shall meet theSemiFuture
requirements. -
!is_reference_v<Future>
[ Note: This constrains the parameter to be an rvalue reference rather than a forwarding reference. — end note ] -
Effects:
-
Blocks the calling thread until
f
becomes ready. -
Retrieves the asynchronous result.
-
Returns:
-
If
f
becomes ready with a value, moves the value fromf
and returns it to the caller.
-
-
Postconditions:
f
is invalid. -
Synchronization: The destruction of the continuation that generates
f
's value synchronizes withfuture_get
calls blocking untilf
becomes ready. -
Throws: If
f
becomes ready with an exception, that exception is rethrown.
2.13. FutureContinuation
Helper Functions
namespace std::execution { template <class F> /* see below */ on_value(F&& f); template<class F> /* see below */ on_error(F&& f); template<class F, class G> /* see below */ on_value_or_error(F&& f, G&& g); }
template<class F> /* see below */ on_value(F&& f);
Requires:
-
For any (possibly cv-qualified) object type
T
,invoke(declval<F>(), declval<T>())
shall be well-formed. -
F
shall meet theMoveConstructible
requirements.
Returns: A FutureContinuation
object, fc
, of implementation-defined type such that:
-
fc(t)
is well-formed for objectst
of typeT
and has the same effects asinvoke(ff, t)
, whereff
is an instance ofF
move-constructed fromforward<F>(f)
. The objectff
is constructed before the return ofon_value()
and destroyed whenfc
is destroyed. -
fc(exception_arg, exception_ptr{})
is ill-formed.template<class F> /* see below */ on_error(F&& f);
Requires:
-
invoke(declval<F>(), declval<exception_ptr>())
shall be well-formed. -
F
shall meet theMoveConstructible
requirements.
Returns: A FutureContinuation
object, fc
, of implementation-defined type such that:
-
fc(exception_tag, e)
is well-formed for objectse
of typeexception_ptr
and has the same effects asinvoke(ff, e)
, whereff
is an object of typeF
move-constructed fromforward<F>(f)
. The objectff
is constructed before the return ofon_error
and destroyed whenfc
is destroyed. -
fc(t)
is ill-formed for any typeT
.template<class F, class G> /* see below */ on_value_or_error(F&& f, G&& g);
Requires:
-
For any (possibly cv-qualified) object type
T
,invoke(declval<F>(), declval<T>())
andinvoke(declval<G>(), declval<exception_ptr>())
shall be well-formed. -
F
andG
shall meet theMoveConstructible
requirements.
Returns: A FutureContinuation
object, fc
, of implementation-defined type such that:
-
fc(t)
is well-formed for objectst
of typeT
and has the same effects asinvoke(ff, t)
, whereff
is an object of typeF
move-constructed fromforward<F>(f)
. The objectff
is constructed before the return ofon_value_or_error
and destroyed whenfc
is destroyed. -
fc(exception_tag, e)
is well-formed for objectse
of typeexception_ptr
and has the same effects asinvoke(gg, e)
, wheregg
is an object of type ofG
move-constructed fromforward<G>(g)
. The objectgg
is constructed before the return ofon_value_or_error
and destroyed whenfc
is destroyed.
2.14. Proposed Modifications to Executors
2.14.1. Future
Requirements
Remove this section.
2.14.2. TwoWayExecutor
Requirements
In the Return Type column:
Replace:
A type that satisfies theFuture
requirements for the value typeR
.
With:
A type that satisfies theContinuableFuture
requirements for the value typeR
.
In the Operational Semantics column:
Replace:
in the associated shared state of the resulting Future
.
With:
in the resulting ContinuableFuture
.
2.14.3. ThenExecutor
Requirements
In the type requirements list:
Replace:
fut
denotes a future object satisfying theFuture
requirements,
With:
fut
denotes a future object that:was returned by a call to
x.twoway_execute
,x.bulk_twoway_execute
,x.then_execute
, orx.bulk_then_execute
and meets theContinuableFuture
requirements.was returned by a call to
execution::query(x, promise_contract_t<T>)
orexecution::query(x, cancellable_promise_contract_t<T>)
.
In the Return Type column:
Replace:
A type that satisfies theFuture
requirements for the value typeR
.
With:
A type that satisfies theContinuableFuture
requirements for the value typeR
.
In the Operational Semantics column:
Replace:
in the associated shared state of the resulting Future
.
With:
in the resulting ContinuableFuture
.
2.14.4. BulkTwoWayExecutor
Requirements
In the Return Type column:
Replace:
A type that satisfies theFuture
requirements for the value typeR
.
With:
A type that satisfies theContinuableFuture
requirements for the value typeR
.
In the Operational Semantics column:
Replace:
in the associated shared state of the resulting Future
.
With:
in the resulting ContinuableFuture
.
2.14.5. BulkThenExecutor
Requirements
In the type requirements list:
Replace:
fut denotes a future object satisfying the Future requirements,
With:
fut denotes a future object that:
was returned by a call to
x.twoway_execute
,x.bulk_twoway_execute
,x.then_execute
, orx.bulk_then_execute
and meets theContinuableFuture
requirements.was returned by a call to
execution::query(x, promise_contract_t<T>)
orexecution::query(x, cancellable_promise_contract_t<T>)
.
In the Return Type column:
Replace:
A type that satisfies theFuture
requirements for the value typeR
.
With:
A type that satisfies theContinuableFuture
requirements for the value typeR
In the Operational Semantics column:
Replace:
in the associated shared state of the resulting Future.
With:
in the resulting ContinuableFuture.
2.14.6. twoway_t
Customization Points
Replace:
it is std::experimental::future<T>
With:
it is a execution::continuable_future<T, E1>
2.14.7. single_t
Customization Points
Replace:
it is std::experimental::future<T>
With:
it is execution::continuable_future<T, E1>
2.14.8. Properties To Indicate If Blocking And Directionality May Be Adapted
Remove twoway_t
from the Requirements column of the Table.
2.14.9. Class Template executor
Replace:
template<class Function> std::experimental::future<result_of_t<decay_t<Function>()>> twoway_execute(Function&& f) const
With:
template<class Function> execution::semi_future<result_of_t<decay_t<Function>()>> twoway_execute(Function&& f) const
Replace:
template<class Function, class ResultFactory, class SharedFactory> std::experimental::future<result_of_t<decay_t<ResultFactory>()>> bulk_twoway_execute(Function&& f, size_t n, ResultFactory&& rf, SharedFactory&& sf) const;
With:
template<class Function, class ResultFactory, class SharedFactory> execution::semi_future<result_of_t<decay_t<ResultFactory>()>> bulk_twoway_execute(Function&& f, size_t n, ResultFactory&& rf, SharedFactory&& sf) const;
2.14.10. executor
Operations
Replace:
template<class Function> std::experimental::future<result_of_t<decay_t<Function>()>> twoway_execute(Function&& f) const
With:
template<class Function> /* implementation-defined future type */ twoway_execute(Function&& f) const
Replace:
Returns: A future, whose shared state is made ready when the future returned bye.twoway_execute(f2)
is made ready, containing the result off1()
or any exception thrown byf1()
. [ Note:e2.twoway_execute(f2)
may return any future type that satisfies theFuture
requirements, and not necessarily One possible implementation approach is for the polymorphic wrapper to attach a continuation to the inner future via that object’sthen()
member function. When invoked, this continuation stores the result in the outer future’s associated shared state and makes that shared state ready. — end note ]
With:
Returns: A value whose type satisfies theContinuableFuture
requirements. The returned future is fulfilled whenf1()
completes execution, with the result off1()
(ifdecltype(f1())
isvoid
), valueless completion (ifdecltype(f1())
isvoid
), or any exception thrown byf1()
.
Replace:
template<class Function, class ResultFactory, class SharedFactory> std::experimental::future<result_of_t<decay_t<ResultFactory>()>> void bulk_twoway_execute(Function&& f, size_t n, ResultFactory&& rf, SharedFactory&& sf) const;
With:
template<class Function, class ResultFactory, class SharedFactory> /* implementation-defined future type */ bulk_twoway_execute(Function&& f, size_t n, ResultFactory&& rf, SharedFactory&& sf) const;
Replace:
Returns: A future, whose shared state is made ready when the future returned by e.bulk_twoway_execute(f2, n, rf2, sf2) is made ready, containing the result in r1 (if decltype(rf1()) is non-void) or any exception thrown by an invocationf1
. [ Note:e.bulk_twoway_execute(f2)
may return any future type that satisfies theFuture
requirements, and not necessarilystd::experimental::future
. One possible implementation approach is for the polymorphic wrapper to attach a continuation to the inner future via that object’sthen()
member function. When invoked, this continuation stores the result in the outer future’s associated shared state and makes that shared state ready. — end note ]
With:
Returns:
A value whose type satisfies theContinuableFuture
requirements. The returned future is fulfilled whenf1()
completes execution, with the result inr1
(ifdecltype(rf1())
isvoid
), valueless completion (ifdecltype(rf1())
is void), or any exception thrown by an invocationf1
.
2.14.11. static_thread_pool
Executor Type
Replace:
template<class Function> std::experimental::future<result_of_t<decay_t<Function>()>> twoway_execute(Function&& f) const template<class Function, class Future> std::experimental::future<result_of_t<decay_t<Function>(decay_t<Future>)>> then_execute(Function&& f, Future&& pred) const; template<class Function, class SharedFactory> void bulk_execute(Function&& f, size_t n, SharedFactory&& sf) const; template<class Function, class ResultFactory, class SharedFactory> std::experimental::future<result_of_t<decay_t<ResultFactory>()>> void bulk_twoway_execute(Function&& f, size_t n, ResultFactory&& rf, SharedFactory&& sf) const;
With:
template<class Function> std::execution::continuable_future<result_of_t<decay_t<Function>()>, C> twoway_execute(Function&& f) const template<class Function, class Future> execution::continuable_future<result_of_t<decay_t<Function>(decay_t<Future>)>, C> then_execute(Function&& f, Future&& pred) const; template<class Function, class SharedFactory> void bulk_execute(Function&& f, size_t n, SharedFactory&& sf) const; template<class Function, class ResultFactory, class SharedFactory> execution::continuable_future<result_of_t<decay_t<ResultFactory>()>>, C> void bulk_twoway_execute(Function&& f, size_t n, ResultFactory&& rf, SharedFactory&& sf) constReplace the same instances in the documentation section below the main code block.