Document Number:N4107
Date:
Revises:N3970
Editor: Artur Laksberg
Microsoft Corp.
[email protected]

Working Draft, Technical Specification for C++ Extensions for Concurrency

Note: this is an early draft. It’s known to be incomplet and incorrekt, and it has lots of bad formatting.

1

General

[general]
1.1

Scope

[general.scope]

This technical specification describes a number of concurrency extensions to the C++ Standard Library (1.2). These extensions are classes and functions that are likely to be used widely within a program and/or on the interface boundaries between libraries written by different organizations.

This technical specification is non-normative. Some of the library components in this technical specification may be considered for standardization in a future version of C++, but they are not currently part of any C++ standard. Some of the components in this technical specification may never be standardized, and others may be standardized in a substantially changed form.

The goal of this technical `specification` is to build more widespread existing practice for an expanded C++ standard library. It gives advice on extensions to those vendors who wish to provide them.

1.2

Normative references

[general.references]

The following referenced document is indispensable for the application of this document. For dated references, only the edition cited applies. For undated references, the latest edition of the referenced document (including any amendments) applies.

  • ISO/IEC 14882:—1, Programming Languages — C++
  • RFC 2781, UTF-16, an encoding of ISO 10646

ISO/IEC 14882:— is herein called the C++ Standard. References to clauses within the C++ Standard are written as "C++14 §3.2". The library described in ISO/IEC 14882:— clauses 17–30 is herein called the C++ Standard Library.

1.3

Namespaces, headers, and modifications to standard classes

[general.namespaces]

Some of the extensions described in this Technical Specification represent types and functions that are currently not part of the C++ Standards Library, and because these extensions are experimental, they should not be declared directly within namespace std. Instead, such extensions are declared in namespace std::experimental.

[ Note: Once standardized, these components are expected to be promoted to namespace std. end note ]

Unless otherwise specified, references to such entities described in this Technical Specification are assumed to be qualified with std::experimental, and references to entities described in the C++ Standard Library are assumed to be qualified with std::.

2

Improvements to std::future<T> and Related APIs

[future]
2.1

General

[futures.general]

The extensions proposed here are an evolution of the functionality of std::future and std::shared_future. The extensions enable wait free composition of asynchronous operations.

2.2

Changes to class template future

[futures.unique_future]

To the class declaration found in C++14 §30.6.6 paragraph 3, add the following to the public functions:


bool is_ready() const;

future(future<future<R>>&& rhs) noexcept;

template<typename F>
see below then(F&& func);

template<typename F>
see below then(executor &ex, F&& func);

template<typename F>
see below then(launch policy, F&& func);

In C++14 §30.6.6 between paragraphs 8 and 9, add the following:

future(future<future<R>>&& rhs) noexcept;
Effects:
Constructs a future object by moving the instance referred to by rhs and unwrapping the inner future.
Postconditions:
  • valid() returns the same value as rhs.valid() prior to the constructor invocation.
  • rhs.valid() == false.

After C++14 §30.6.6 paragraph 26, add the following:


template<typename F>
see below then(F&& func);

template<typename F>
see below then(executor &ex, F&& func);

template<typename F>
see below then(launch policy, F&& func);
Notes:
The threetwo functions differ only by input parameters. The first only takes a callable object which accepts a future object as a parameter. The second function takes an executor as the first parameter and a callable object as the second parameter. The thirdsecond function takes a launch policy as the first parameter and a callable object as the second parameter.
Effects:

  • The continuation INVOKE(DECAY_COPY (std::forward<F>(func))) is called when the object's shared state is ready (has a value or exception stored).
  • The continuation launches according to the specified launch policy or executor.
  • When the executor or launch policy is not provided the continuation inherits the parent's launch policy or executor.
  • Any value returned from the continuation is stored as the result in the shared state of the resulting future. Any exception propagated from the execution of the continuation is stored as the exceptional result in the shared state of the resulting future.
  • If the parent was created with std::promise or with a packaged_task (has no associated launch policy), the continuation behaves the same as the thirdsecond overload with a policy argument of launch::async | launch::deferred and the same argument for func.
  • If the parent has a policy of launch::deferred, then it is filled by calling wait() or get() on the resulting future. [ Example:
    auto f1 = async(launch::deferred, [] { return 1; });
    
    auto f2 = f1.then([](future n) { return 2; });
    
    f2.wait(); // execution of f1 starts here, followed by f2
          
    end example ]
Returns:
The return type of then depends on the return type of the closure func as defined below:
  • When result_of_t<decay_t<F>()> is future<R>, the function returns future<R>.
  • Otherwise, the function returns future<result_of_t<decay_t<F>()>>.
    Notes:
    The first rule above is called the implicit unwrapping. Without this rule, the return type of then taking a closure returning a future<R> would have been future<future<R>>. This rule avoids such nested future objects. [ Example: The type of f2 below is future<int> and not future<future<int>>:
    future<int> f1 = g();
    future<int> f2 = f1.then([](future<int> f) {
                        future<int> f3 = h();
                        return f3;
                     });
    
    end example ]
Postconditions:
  • The future object is moved to the parameter of the continuation function.
  • valid() == false on original future object immediately after it returns.
bool is_ready() const;
Returns:
true if the shared state is ready, false if it isn't.
2.3

Changes to class template shared_future

[futures.shared_future]

To the class declaration found in C++14 §30.6.7 paragraph 3, add the following to the public functions:

bool is_ready() const;

template<typename F>
see below then(F&& func);

template<typename F>
see below then(executor &ex, F&& func);

template<typename F>
see below then(launch policy, F&& func);

After C++14 §30.6.7 paragraph 28, add the following:


template<typename F>
see below shared_future::then(F&& func);

template<typename F>
see below shared_future::then(executor &ex, F&& func);

template<typename F>
see below shared_future::then(launch policy, F&& func);
Notes:
The threetwo functions differ only by input parameters. The first only takes a callable object which accepts a shared_future object as a parameter. The second function takes an executor as the first parameter and a callable object as the second parameter. The thirdsecond function takes a launch policy as the first parameter and a callable object as the second parameter.
Effects:
  • The continuation INVOKE(DECAY_COPY (std::forward<F>(func))) is called when the object's shared state is ready (has a value or exception stored).
  • The continuation launches according to the specified policy or executor.
  • When the scheduler or launch policy is not provided the continuation inherits the parent's launch policy or executor.
  • Any value returned from the continuation is stored as the result in the shared state of the resulting future. Any exception propagated from the execution of the continuation is stored as the exceptional result in the shared state of the resulting future.
  • If the parent was created with std::promise (has no associated launch policy), the continuation behaves the same as the thirdsecond function with a policy argument of launch::async | launch::deferred and the same argument for func.
  • If the parent has a policy of launch::deferred, then it is filled by calling wait() or get() on the resulting shared_future. [ Note: This is similar to future. See example in 2.2. end note ]
Returns:
The return type of then depends on the return type of the closure func as defined below:
  • When result_of_t<decay_t<F>()> is future<R>, the function returns future<R>.
  • Otherwise, the function returns future<result_of_t<decay_t<F>()>>.
    Notes:
    This is the same as in future. See the notes on future::then return type in 2.2.
Postconditions:
  • The shared_future passed to the continuation function is a copy of the original shared_future.
  • valid() == true on the original shared_future object.
bool is_ready() const;
Returns:
true if the shared state is ready, false if it isn't.
2.4

Function template when_all

[futures.when_all]

A new section 30.6.10 shall be inserted at the end of C++14 §30.6. Below is the content of that section.


template 
see below when_all(InputIterator first, InputIterator last);

template <typename... T>
see below when_all(T&&... futures);
Requires:
T is of type future<R> or shared_future<R>.
Notes:
  • There are two variations of when_all. The first version takes a pair of InputIterators. The second takes any arbitrary number of future<R0> and shared_future<R1> objects, where R0 and R1 need not be the same type.
  • Calling the first signature of when_all where InputIterator first equals last, returns a future with an empty vector that is immediately ready.
  • Calling the second signature of when_any with no arguments returns a future<tuple<>> that is immediately ready.
Effects:
  • Each future and shared_future is waited upon and then copied into the collection of the output (returned) future, maintaining the order of the futures in the input collection.
  • The future returned by when_all will not throw an exception, but the futures held in the output collection may.
Returns:
  • future<tuple<>> if when_all is called with zero arguments.
  • future<vector<future<R>>> if the input cardinality is unknown at compile and the iterator pair yields future<R>. R may be void. The order of the futures in the output vector will be the same as given by the input iterator.
  • future<vector<shared_future<R>>> if the input cardinality is unknown at compile time and the iterator pair yields shared_future<R>. R may be void. The order of the futures in the output vector will be the same as given by the input iterator.
  • future<tuple<future<R0>, future<R1>, future<R2>...>> if inputs are fixed in number. The inputs can be any arbitrary number of future and shared_future objects. The type of the element at each position of the tuple corresponds to the type of the argument at the same position. Any of R0, R1, R2, etc. may be void.
Postconditions:
  • All input future<T>s valid() == false.
  • All output shared_future<T> valid() == true.
2.5

Function template when_any

[futures.when_any]

A new section 30.6.11 shall be inserted at the end of C++14 §30.6. Below is the content of that section.


template <class InputIterator>
see below when_any(InputIterator first, InputIterator last);

template <typename... T>
see below when_any(T&&... futures);
Requires:
T is of type future<R> or shared_future<R>.
Notes:
  • There are two variations of when_any. The first version takes a pair of InputIterators. The second takes any arbitrary number of future<R> and shared_future<R> objects, where R need not be the same type.
  • Calling the first signature of when_any where InputIterator first equals last, returns a future with an empty vector that is immediately ready.
  • Calling the second signature of when_any with no arguments returns a future<tuple<>> that is immediately ready.
Effects:
  • Each future and shared_future is waited upon. When at least one is ready, all the futures are copied into the collection of the output (returned) future, maintaining the order of the futures in the input collection.
  • The future returned by when_any will not throw an exception, but the futures held in the output collection may.
Returns:
  • future<tuple<>> if when_any is called with zero arguments.
  • future<vector<future<R>>> if the input cardinality is unknown at compile time and the iterator pair yields future<R>. R may be void. The order of the futures in the output vector will be the same as given by the input iterator.
  • future<vector<shared_future<R>>> if the input cardinality is unknown at compile time and the iterator pair yields shared_future<R>. R may be void. The order of the futures in the output vector will be the same as given by the input iterator.
  • future<tuple<future<R0>, future<R1>, future<R2>...>> if inputs are fixed in number. The inputs can be any arbitrary number of future and shared_future objects. The type of the element at each position of the tuple corresponds to the type of the argument at the same position. Any of R0, R1, R2, etc. maybe void.
Postconditions:
  • All input future<T>s valid() == false.
  • All input shared_future<T> valid() == true.
2.6

Function template when_any_back

[futures.when_any_back]

A new section 30.6.12 shall be inserted at the end of C++14 §30.6. Below is the content of that section.


template <class InputIterator>
see below when_any_back(InputIterator first, InputIterator last);
Requires:
InputIterator's value type shall be convertible to future<R> or shared_future<R>. All R types must be the same.
Notes:
  • The function when_any_back takes a pair of InputIterators.
  • Calling when_any_back where InputIterator first equals last, returns a future with an empty vector that is immediately ready.
Effects:
  • Each future and shared_future is waited upon. When at least one is ready, all the futures are copied into the collection of the output (returned) future.
  • After the copy, the future or shared_future that was first detected as being ready swaps its position with that of the last element of the result collection, so that the ready future or shared_future may be identified in constant time. Only one future or shared_future is thus moved.
  • The future returned by when_any_back will not throw an exception, but the futures held in the output collection may.
Returns:
  • future<vector<future<R>>> if the input cardinality is unknown at compile time and the iterator pair yields future<R>. R may be void.
  • future<vector<shared_future<R>>> if the input cardinality is unknown at compile time and the iterator pair yields shared_future<R>. R may be void.
Postconditions:
  • All input future<T>s valid() == false.
  • All input shared_future valid() == true.
2.7

Function template make_ready_future

[futures.make_ready_future]

A new section 30.6.13 shall be inserted at the end of C++14 §30.6. Below is the content of that section.


  template <typename T>
  future<decay_t<T>> make_ready_future(T&& value);

  future<void> make_ready_future();
  
Effects:
The value that is passed in to the function is moved to the shared state of the returned future if it is an rvalue. Otherwise the value is copied to the shared state of the returned future.
Returns:
  • future<decay_t<T>>, if function is given a value of type T.
  • future<void>, if the function is not given any inputs.
Postconditions:
  • Returned future<decay_t<T>>, valid() == true.
  • Returned future<decay_t<T>>, is_ready() == true.
?

Function template async

[futures.async]

Change C++14 §30.6.8 paragraph 1 as follows:

The function template async provides a mechanism to launch a function potentially in a new thread and provides the result of the function in a future object with which it shares a shared state.

template <class F, class... Args>
future<result_of_t<decay_t<F>(decay_t<Args>...)>>
async(F&& f, Args&&... args);

template <class F, class... Args>
future<result_of_t<decay_t<F>(decay_t<Args>...)>>
async(launch policy, F&& f, Args&&... args);


template<class F, class... Args>
future<result_of_t<decay_t<F>(decay_t<Args>...)>>
async(executor& ex, F&& f, Args&&... args);

Change C++14 §30.6.8 paragraph 3 as follows:
Effects:
The first function behaves the same as a call to the second function with a policy argument of launch::async | launch::deferred and the same arguments for F and Args. The second and third functions creates a shared state that is associated with the returned future object. The further behavior of the second function depends on the policy argument as follows (if more than one of these conditions applies, the implementation may choose any of the corresponding policies):
  • if policy & launch::async is non-zero — calls INVOKE (DECAY_COPY (std::forward<F>(f)), DECAY_COPY (std::forward<Args>(args))...) (20.8.2, 30.3.1.2) as if in a new thread of execution represented by a thread object with the calls to DECAY_COPY () being evaluated in the thread that called async. Any return value is stored as the result in the shared state. Any exception propagated from the execution of INVOKE (DECAY_COPY (std::forward<F>(f)), DECAY_COPY (std::forward<Args>(args))...) is stored as the exceptional result in the shared state. The thread object is stored in the shared state and affects the behavior of any asynchronous return objects that reference that state.
  • if policy & launch::deferred is non-zero — Stores DECAY_COPY(std::forward<F>(f)) and DECAY_COPY (std::forward<Args>(args))... in the shared state. These copies of f and args constitute a deferred function. Invocation of the deferred function evaluates INVOKE std::move(g), std::move(xyz)) where g is the stored value of DECAY_COPY (std::forward<F>(f)) and xyz is the stored copy of DECAY_COPY (std::forward<Args>(args)).... The shared state is not made ready until the function has completed. The first call to a non-timed waiting function (30.6.4) on an asynchronous return object referring to this shared state shall invoke the deferred function in the thread that called the waiting function. Once evaluation of INVOKE (std::move(g), std::move(xyz)) begins, the function is no longer considered deferred. [ Note: If this policy is specified together with other policies, such as when using a policy value of launch::async | launch::deferred, implementations should defer invocation or the selection of the policy when no more concurrency can be effectively exploited. end note ]
  • If no value is set in the launch policy, or a value is set that is neither specified in this International Standard or by the implementation, the behaviour is undefined.
The further behavior of the third function is as follows:
The executor::add() function is given a function<void()> which calls INVOKE (DECAY_COPY (std::forward<F>(f)) DECAY_COPY (std::forward<Args>(args))...). The implementation of the executor is decided by the programmer.