Document Number:09-0078=N2888
Date:2009-06-18
Author:Anthony Williams
Just Software Solutions Ltd

N2888: Moving Futures - Proposed Wording for UK comments 335, 336, 337 and 338

UK comments 335 (which is also LWG issue 1048), 336, 337 and 338 in the CD1 ballot propose changes to the working draft to support working with std::unique_future<> and std::shared_future<>. This paper provides additional rationale for the suggested changes, along with proposed wording based on N2857.

Moving std::unique_future<>

In the current working draft (N2857), std::unique_future<> is MoveConstructible but not MoveAssignable. This is a strange state of affairs that leads to bizarre consequences. For example, you can move from an instance of std::unique_future<>, leaving an empty shell, but not move into an instance to provide meaningful operations. e.g.

std::unique_future<int> make_future();

void foo()
{
    std::unique_future<int> f1(make_future());
    std::unique_future<int> f2(std::move(f1)); // OK, f1 now useless
    f1=make_future(); // Error, no move-assignment operator
}

Containers of std::unique_future<>

The fact that std::unique_future<> is MoveConstructible means that you can store instances in a container such as std::vector<>, but this container is severely limited. For example, you can call push_back on a std::vector<std::unique_future<int>>, but not insert, and you can delete elements from the end by using pop_back, but not by calling erase.

void bar()
{
    std::vector<unique_future<int>> vec;
    vec.push_back(make_future());        // OK
    vec.insert(vec.end(),make_future()); // error, requires MoveAssignable
    vec.pop_back();                      // OK
    vec.erase(vec.begin());              // error, requires MoveAssignable
}

Use of futures with standard containers is an important feature. An algorithm written to make use of multiple threads might well divide its work into multiple sub-tasks, and store a std::unique_future<> for each in some form of container. Making it easy to manipulate this container is very important for this primary use case.

As an aside, it is worth noting that since std::unique_future<> is MoveConstructible, you can emulate move-assignment with destruction and move-construction:

template<typename T>
void move_assign(std::unique_future<T> & target,
                 std::unique_future<T> && source)
{
    target.˜std::unique_future<T>();
    new (&target) std::unique_future<T>(std::move(source));
}

You can even write a concept_map that does this for particular instantiations of std::unique_future<UserDefinedType>, though the standard prohibits you from doing this for instantiations that don't involve a user-defined type:

struct MyType{};

concept_map std::MoveAssignable<std::unique_future<MyType>>
{
    std::unique_future<MyType>& operator=(
        std::unique_future<MyType>&& rhs)
    {
        this->˜std::unique_future<MyType>();
        new (this) std::unique_future<MyType>(std::move(rhs));
        return *this;
    }
}

Such a concept map would allow you to use move-assignment seamlessly in constrained templates such as std::vector<>, but only for instantiations involving a user-defined type.

Given that you can write such code, it seems most unfortunate that you cannot just write a direct move-assignment in unconstrained code:

    target=std::move(source);

For the reasons presented above, UK comment 336 proposes that a move-assignment operator is added to std::unique_future<>.

Default construction of std::unique_future<>

Even with a move-assignment operator, std::unique_future<> is still severely limited, since it lacks a default constructor so you must construct it with a value. This prevents calling resize on a std::vector<std::unique_future<>>, and again limits the use of std::unique_future<> for holding the results of sub-tasks — it can be beneficial to be able to allocate the container to hold the results prior to actually starting the tasks and obtaining the correct futures. Preventing default construction makes this unnecessarily difficult. It also means that arrays can only be created if every element is initialized, which is not always readily possible.

For example, if your algorithm can be divided into a number of predefined steps, it may make sense to create an array of futures to hold the results of the subtasks in order to avoid the overhead of additional dynamic memory allocation. This is made considerably easier if the futures support default construction, since the already-constructed futures can be waited-for if subsequent tasks cannot be started for some reason, rather than just being destroyed. For example:

class task_data{};
class result_type{};

unsigned const num_steps=42;
std::unique_future<result_type> run_subtask(task_data& state,unsigned step);

void parallel_task()
{
    task_data state;
    std::unique_future<result_type> results[num_steps];
    unsigned count=0;
    try
    {
        for(count=0;count<num_steps;++count)
        {
            results[count]=run_subtask(state,count);
        }
    }
    catch(...)
    {
        state.set_cancel_flag();
        for(unsigned i=count;i!=0;)
        {
            --i;
            results[i].wait();
        }
        throw;
    }
}

Such a construction becomes more difficult if std::unique_future<> doesn't support default construction.

At first glance, omitting default construction prevents the construction of std::unique_future<> values without an associated state. However, this is not the case: such an instance can be obtained by using an object as the source of a move-construction:

void foo()
{
    std::unique_future<int> f1(make_future());
    std::unique_future<int> f2(std::move(f1));
    // f1 is now "empty", just like a default-constructed instance would be
}

Therefore UK comment 336 also proposes to allow default construction of std::unique_future<>.

Move-assignment for std::shared_future<>

std::shared_future<> is currently CopyConstructible, but not CopyAssignable or MoveAssignable. As for std::unique_future<>, this can lead to bizarre restrictions on the use of the type with containers such as std::vector<>.

The key benefit to be obtained from the lack of assignment is that instances are immutable. However, declaring individual instances const provides this property in a way much more consistent with the rest of the language.

As for std::unique_future<>, it therefore makes sense to make std::shared_future<> MoveAssignable too.

Since std::shared_future<> is CopyConstructible, many of the use cases for making it MoveAssignable can be addressed by making it CopyAssignable instead. However, the UK position is that move-assignment is still beneficial. See the section on move-construction for std::shared_future<> below for further rationale on that point.

Incidentally, disallowing move-assignment does not prevent the existence of std::shared_future<> instances with no associated state, since you could construct such an instance by move-construction from an already-moved-from std::unique_future<> instance:

    std::unique_future<int> uf1=make_future();
    std::unique_future<int> uf2(std::move(uf1));
    std::shared_future<int> sf1(std::move(uf1));

UK comment 338 therefore proposes that std::shared_future<> be made MoveAssignable by the addition of a move-assignment operator.

Copy-assignment for std::shared_future<>

Given a CopyConstructible and MoveAssignable type, it is possible to perform copy-assignment by move-assignment from a copy of the original:

    std::shared_future<int> sf1=make_shared_future();
    std::shared_future<int> sf2=make_another_shared_future();

    sf2=std::shared_future<int>(sf1); // move-assign from a copy

If the desirability of move-assignment for std::shared_future<> is accepted, it therefore seems a natural extension to allow copy assignment.

UK comment 338 therefore proposes that std::shared_future<> be made CopyAssignable by the addition of a copy-assignment operator.

Move-construction for std::shared_future<>

By its very nature, multiple instances of std::shared_future<> can refer to the same associated state. This means that in order for the state to be correctly destroyed when the last future that references it is destroyed some kind of reference counting scheme must be used. If std::shared_future<> only has a copy constructor and not a move constructor then this count must be updated even if the source is a temporary that is about to be destroyed. In this case, if the copy cannot be elided then the count must be incremented during the construction of the copy and then decremented during the destruction of the temporary. Atomic operations are expensive, so this is an unnecessary performance drain.

The same problem existed for std::shared_ptr<>, and for this reason std::shared_ptr<> has a move constructor and move-assignment operator in addition to the copy constructor and copy-assignment operator. This was introduced in N2351: Improving shared_ptr for C++0x, revision 2.

Though it could be argued that this is a Quality-of-Implementation issue, the consequences of not allowing move semantics for std::shared_future<> can be a noticeable performance impact, particularly with containers of such objects. Also, not only does such unnecessary incrementing and decrementing of the counter affect the performance of the current thread, it can also affect the performance of other threads which are accessing std::shared_future<> instances that reference the same associated state, due to cache line contention.

As an important optimization, and for consistency with std::shared_ptr<>, UK comments 337 and 338 therefore propose that std::shared_future<> should have a move constructor and move-assignment operator.

Detecting futures without an associated value

If you move from a std::unique_future<> or std::shared_future<> then the source object has no associated value. This means that calling the wait or get member functions is not permitted. If the proposal to allow default construction of futures is accepted, then the same problem will exist for such default-constructed futures.

This allows for better handling of containers of futures where individual elements may or may not have associated state, as you can erase or reuse elements that have no associated state, or avoid waiting for such elements.

UK comment 335 proposes to address this by the provision of a new member function waitable which can always be safely called on an instance of a future. If a call to waitable on a given object returns false then the object has no associated value, so calling wait or get will throw. On the other hand, if a call to waitable returns true then the object does have an associated value, so calls to wait and get should succeed. LWG issue 1048 has been opened from this comment, but currently lacks proposed wording.

Proposed Wording

These wording changes are based on the current working paper, N2857. Additions and deletions are marked in green and red as shown here.

Changes to 30.6.4 [futures.unique_future]

Update the class synopsis of std::unique_future as follows:

template<class R>
class unique_future {
public:
    unique_future();
    unique_future(const unique_future& rhs) = delete;
    unique_future(unique_future&& rhs);
    ˜unique_future();
    unique_future & operator=(const unique_future& rhs) = delete;
    unique_future& operator=(unique_future&& rhs);

    bool waitable() const;
    // retrieving the value
    see below get() const;

    // functions to check state and wait for ready
    bool is_ready() const;
    bool has_exception() const;
    bool has_value() const;

    void wait() const;
    template <class Rep, class Period>
    bool wait_for(const chrono::duration<Rep, Period>& rel_time) const;
    template <class Clock, class Duration>
    bool wait_until(const chrono::time_point<Clock, Duration>& abs_time) const;
};

Add the following after current paragraph 1:

unique_future();
Effects:
Construct a new instance of unique_future with no associated state.
Postcondition:
waitable() returns false

Modify the current paragraph 3 (postcondition of move-constructor) as follows:

Postcondition:
rhs can be safely destroyed. Calling waitable on the newly-constructed object returns the same value as rhs.waitable() prior to the constructor invocation. rhs.waitable() returns false.

Add the following after the current paragraph 4:

unique_future& operator=(unique_future&& rhs);
Effects:
Transfer the state associated with rhs to this. If this->waitable() was true prior to the assignment, and there are no promise or packaged_task instances referencing that state, then destroy that state.
Postcondition:
Calling waitable on the newly-constructed object returns the same value as rhs.waitable() prior to the assignment invocation. rhs.waitable() returns false.
bool waitable() const;
Returns:
true if this has associated state, false otherwise.

Add a new paragraph following the current paragraph 5 as part of the description of unique_future::get():

Precondition:
waitable() returns true.

Add a new paragraph prior to the current paragraph 13 as part of the description of unique_future::wait():

Precondition:
waitable() returns true.

Add a new paragraph prior to the current paragraph 16 as part of the description of unique_future::wait_for():

Precondition:
waitable() returns true.

Add a new paragraph prior to the current paragraph 19 as part of the description of unique_future::wait_until():

Precondition:
waitable() returns true.

Changes to 30.6.5 [futures.shared_future]

Alter the class synopsis of std::shared_future as follows:

template<class R>
class shared_future {
public:
    shared_future();
    shared_future(const shared_future& rhs);
    shared_future(shared_future&& rhs);
    shared_future(unique_future<R>);
    ˜shared_future();
    shared_future & operator=(const shared_future& rhs) = delete;
    shared_future& operator=(shared_future&& rhs);

    bool waitable() const;
    // retrieving the value
    see below get() const;
    // functions to check state and wait for ready
    bool is_ready() const;
    bool has_exception() const;
    bool has_value() const;
    void wait() const;
    template <class Rep, class Period>
    bool wait_for(const chrono::duration<Rep, Period>& rel_time) const;
    template <class Clock, class Duration>
    bool wait_until(const chrono::time_point<Clock, Duration>& abs_time) const;
};

Add the following after current paragraph 1:

shared_future();
Effects:
Construct a new instance of shared_future with no associated state.
Postcondition:
waitable() returns false

Added the following after the current paragraph 2 (effects of copy-constructor) as follows:

Postcondition:
Calling waitable on the newly-constructed object returns the same value as rhs.waitable().
shared_future(shared_future&& rhs);
Effects:
Transfer the state associated with rhs to this.
Postcondition:
Calling waitable on the newly-constructed object returns the same value as rhs.waitable() prior to the constructor invocation. rhs.waitable() returns false.
shared_future& operator=(const shared_future& rhs);
Effects:
Update this to reference the state associated with rhs. If this->waitable() was true prior to the assignment, and there are no promise, packaged_task or other shared_future instances referencing that state, then destroy that state.
Postcondition:
Calling waitable on the newly-constructed object returns the same value as rhs.waitable() prior to the assignment invocation. this and rhs reference the same associated state.
shared_future& operator=(shared_future&& rhs);
Effects:
Transfer the state associated with rhs to this. If this->waitable() was true prior to the assignment, and there are no promise, packaged_task or other shared_future instances referencing that state, then destroy that state.
Postcondition:
Calling waitable on the newly-constructed object returns the same value as rhs.waitable() prior to the assignment invocation. rhs.waitable() returns false.
bool waitable() const;
Returns:
true if this has associated state, false otherwise.

Add a new paragraph following the current paragraph 7 as part of the description of shared_future::get():

Precondition:
waitable() returns true.

Add a new paragraph prior to the current paragraph 13 as part of the description of shared_future::wait():

Precondition:
waitable() returns true.

Add a new paragraph prior to the current paragraph 16 as part of the description of shared_future::wait_for():

Precondition:
waitable() returns true.

Add a new paragraph prior to the current paragraph 19 as part of the description of shared_future::wait_until():

Precondition:
waitable() returns true.

Acknowledgements

I am grateful to Alisdair Meredith and Jonathan Wakely for their comments on drafts of this paper.