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 refinesSemiFutureand instances can have oneFutureContinuationattached to them (f.then(c)), which is executed on the future’s associated executor when the future becomes ready. -
SharedFuture, which refinesContinuableFutureand instances can have multipleFutureContinuations 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.
.thenblocks until the producer execution agent signals readiness. -
Producer Side: The producer execution agent always executes the continuation.
.set_valueblocks until the consumer execution agent signals readiness. -
inline_executorSemantics: 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_executorSemantics: A newstd::threadexecutes 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_executeorbulk_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 ContinuableFutures and let c be a FutureContinuation.
g = f.then(c);
When f is fulfilled:
-
If
fhas a valueval: -
If
c(val)is well-formed, it is called and the futuregis fulfilled with the result or any exception thrown from the evaluation. -
Otherwise, the future
gis fulfilled with the valueval(this is known as future value propagation - see [P0701r0] and [P0701r1]). -
If
ffails with exceptionexc: -
If
c(exception_arg, exc)is well-formed, it is called and the futuregis fulfilled with the result or any exception thrown from the evaluation. -
Otherwise, the future
gis 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 SharedFutures are also SemiFutures, .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::promiseinteroperability. -
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_tis an empty structure type used as a unique type to disambiguateFutureContinuationoverloads that are called when a future holds an exception fromFutureContinuationoverloads 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
BinaryTypeTraitwith a base characteristic oftrue_typeif the corresponding condition is true, andfalse_typeotherwise. -
A
FutureContinuationtype shall meet theMoveConstructiblerequirements 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::pairwhere 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
SemiFuturetype for typeTshall meet theMoveConstructiblerequirements, theMoveAssignablerequirements, 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
ContinuableFutureshall meet theSemiFuturerequirements 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
SharedFutureshall meet theContinuableFuturerequirements, theCopyConstructiblerequirements, theCopyAssignablerequirements 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
promiseobject that does not refer to a promise -
Postconditions:
-
valid() == false.
-
promise(promise&& rhs) noexcept;
-
Effects: Move constructs a
promiseobject fromrhsthat refers to the same promise and is associated with the same future (ifrhsrefers 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:
-
Promisemeets 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
promiseobject that refers to the promiserhsand is associated with the same future asrhs. (ifrhsis 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*thisrefers to. -
For
promise::set_value(T&& val) &&: equivalent to callingset_value(std::move(val))on the promise that*thisrefers to. -
For
promise<void>::set_value() &&: equivalent to callingset_value()on the promise that*thisrefers to.
-
-
Otherwise, throws
-
Throws:
future_errorwith error conditionno_stateifvalid() == 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:
-
Erroris contextually convertible toexception_ptr -
Effects:
-
If
valid() == true, equivalent to callingset_exception(exception_ptr{forward<Error>(err)})on the promise that*thisrefers to. -
Otherwise, throws
-
Throws:
future_errorwith error conditionno_stateifvalid() == false -
Postconditions:
-
valid() == false
bool valid() const noexcept; explicit operator bool() const noexcept;
-
Returns:
trueif*thisrefers to a promise and the referenced promise isvalid(), orfalseotherwise
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
rhsthat refers to the same future and its associated with the same promise (ifrhsrefers 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
rhsthat refers to the same future and its associated with the same promise (ifrhsrefers 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
rhsthat refers to the same future and its associated with the same promise (ifrhsrefers 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
rhsthat refers to the same future and its associated with the same promise (ifrhsrefers 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:
-
eis aThenExecutorwheremake_promise_contract(e)is well-formed. -
eis aOnewayExecutoror 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
rhsthat refers to the same future and its associated with the same promise (ifrhsrefers 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
rhsthat refers to the same future and its associated with the same promise (ifrhsrefers 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
rhsthat refers to the same future and its associated with the same promise (ifrhsrefers 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
rhsthat refers to the same future and its associated with the same promise (ifrhsrefers 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
ContinuableFuturethat is bound to the executoreand that wraps the type returned by execution of either the value or exception operations implemented in the continuation. The type of the returnedContinuableFutureis defined byExecutorE. -
Effects:
-
For
NORMALdefined as the expressionDECAY_COPY(std::forward<F>(g))(val)ifTis non-void andDECAY_COPY(std::forward<F>(g))()ifTis void. -
For
EXCEPTIONALdefined as the expressionDECAY_COPY(std::forward<F>(f))(exception_arg, ex), -
When
*thisbecomes nonexceptionally ready, and ifNORMALis a well-formed expression, creates an execution agent which invokesNORMALat most once, with the call toDECAY_COPYbeing evaluated in the thread that called.then. -
Otherwise, when
*thisbecomes exceptionally ready, ifEXCEPTIONALis a well-formed expression, creates an execution agent which invokesEXCEPTIONALat most once, with the call toDECAY_COPYbeing evaluated in the thread that called.then. -
If
NORMALandEXCEPTIONALare both well-formed expressions,decltype(EXCEPTIONAL)shall be convertible toR. -
If
NORMALis not a well-formed expression andEXCEPTIONALis a well-formed expression,decltype(EXCEPTIONAL)shall be convertible todecltype(val). -
If neither
NORMALnorEXCEPTIONALare well-formed expressions, the invocation of.thenshall be ill-formed. -
May block pending completion of
NORMALorEXCEPTIONAL. -
The invocation of
.thensynchronizes with (C++Std [intro.multithread]) the invocation off. -
Stores the result of either the
NORMALorEXCEPTIONALexpression, or any exception thrown by either, in the associated shared state of the resultingContinuableFuture. Otherwise, stores eithervalorein 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:
-
eis aThenExecutorwheremake_promise_contract(e)is well-formed. -
eis aOnewayExecutoror 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
ContinuableFuturethat is bound to the executoreand that wraps the type returned by execution of either the value or exception operations implemented in the continuation. The type of the returnedContinuableFutureis defined byExecutorE. -
Effects:
-
For
NORMALdefined as the expressionDECAY_COPY(std::forward<F>(g))(val)ifTis non-void andDECAY_COPY(std::forward<F>(g))()ifTis void. -
For
EXCEPTIONALdefined as the expressionDECAY_COPY(std::forward<F>(f))(exception_arg, ex), -
When
*thisbecomes nonexceptionally ready, and ifNORMALis a well-formed expression, creates an execution agent which invokesNORMALat most once, with the call toDECAY_COPYbeing evaluated in the thread that called.then. -
Otherwise, when
*thisbecomes exceptionally ready, ifEXCEPTIONALis a well-formed expression, creates an execution agent which invokesEXCEPTIONALat most once, with the call toDECAY_COPYbeing evaluated in the thread that called.then. -
If
NORMALandEXCEPTIONALare both well-formed expressions,decltype(EXCEPTIONAL)shall be convertible toR. -
If
NORMALis not a well-formed expression andEXCEPTIONALis a well-formed expression,decltype(EXCEPTIONAL)shall be convertible todecltype(val). -
If neither
NORMALnorEXCEPTIONALare well-formed expressions, the invocation of.thenshall be ill-formed. -
May block pending completion of
NORMALorEXCEPTIONAL. -
The invocation of
.thensynchronizes with (C++Std [intro.multithread]) the invocation off. -
Stores the result of either the
NORMALorEXCEPTIONALexpression, or any exception thrown by either, in the associated shared state of the resultingContinuableFuture. Otherwise, stores eithervalorein 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:
-
eis aThenExecutorwheremake_promise_contract(e)is well-formed. -
eis aOnewayExecutoror 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:
Futuremeets the requirements ofSemiFuture. -
Effects: Blocks the calling thread until
fbecomes ready. -
Synchronization: The destruction of the continuation that fulfills
f's synchronizes withfuture_waitcalls blocking untilfbecomes ready.template<class Future> future_value_t<decay_t<Future>> future_get(Future&& f);
-
Requires:
-
decay_t<Future>shall meet theSemiFuturerequirements. -
!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
fbecomes ready. -
Retrieves the asynchronous result.
-
Returns:
-
If
fbecomes ready with a value, moves the value fromfand returns it to the caller.
-
-
Postconditions:
fis invalid. -
Synchronization: The destruction of the continuation that generates
f's value synchronizes withfuture_getcalls blocking untilfbecomes ready. -
Throws: If
fbecomes 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. -
Fshall meet theMoveConstructiblerequirements.
Returns: A FutureContinuation object, fc, of implementation-defined type such that:
-
fc(t)is well-formed for objectstof typeTand has the same effects asinvoke(ff, t), whereffis an instance ofFmove-constructed fromforward<F>(f). The objectffis constructed before the return ofon_value()and destroyed whenfcis 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. -
Fshall meet theMoveConstructiblerequirements.
Returns: A FutureContinuation object, fc, of implementation-defined type such that:
-
fc(exception_tag, e)is well-formed for objectseof typeexception_ptrand has the same effects asinvoke(ff, e), whereffis an object of typeFmove-constructed fromforward<F>(f). The objectffis constructed before the return ofon_errorand destroyed whenfcis 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. -
FandGshall meet theMoveConstructiblerequirements.
Returns: A FutureContinuation object, fc, of implementation-defined type such that:
-
fc(t)is well-formed for objectstof typeTand has the same effects asinvoke(ff, t), whereffis an object of typeFmove-constructed fromforward<F>(f). The objectffis constructed before the return ofon_value_or_errorand destroyed whenfcis destroyed. -
fc(exception_tag, e)is well-formed for objectseof typeexception_ptrand has the same effects asinvoke(gg, e), whereggis an object of type ofGmove-constructed fromforward<G>(g). The objectggis constructed before the return ofon_value_or_errorand destroyed whenfcis 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 theFuturerequirements for the value typeR.
With:
A type that satisfies theContinuableFuturerequirements 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:
futdenotes a future object satisfying theFuturerequirements,
With:
futdenotes a future object that:was returned by a call to
x.twoway_execute,x.bulk_twoway_execute,x.then_execute, orx.bulk_then_executeand meets theContinuableFuturerequirements.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 theFuturerequirements for the value typeR.
With:
A type that satisfies theContinuableFuturerequirements 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 theFuturerequirements for the value typeR.
With:
A type that satisfies theContinuableFuturerequirements 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_executeand meets theContinuableFuturerequirements.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 theFuturerequirements for the value typeR.
With:
A type that satisfies theContinuableFuturerequirements 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 theFuturerequirements, 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 theContinuableFuturerequirements. 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 theFuturerequirements, 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 theContinuableFuturerequirements. 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.