Document Number: | 09-0078=N2888 |
Date: | 2009-06-18 |
Author: | Anthony
Williams Just Software Solutions Ltd |
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.
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 }
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<>
.
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<>
.
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.
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.
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.
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.
These wording changes are based on the current working paper, N2857. Additions and deletions are marked in green and red as shown here.
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();
unique_future
with no
associated state.waitable()
returns false
Modify the current paragraph 3 (postcondition of move-constructor) as follows:
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);
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.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;
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()
:
waitable()
returns true
.Add a new paragraph prior to the current paragraph 13 as part of
the description of unique_future::wait()
:
waitable()
returns true
.Add a new paragraph prior to the current paragraph 16 as part of
the description of unique_future::wait_for()
:
waitable()
returns true
.Add a new paragraph prior to the current paragraph 19 as part of
the description of unique_future::wait_until()
:
waitable()
returns true
.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();
shared_future
with no
associated state.waitable()
returns false
Added the following after the current paragraph 2 (effects of copy-constructor) as follows:
waitable
on the newly-constructed object returns the same value
as rhs.waitable()
.shared_future(shared_future&& rhs);
rhs
to this
.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);
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.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);
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.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;
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()
:
waitable()
returns true
.Add a new paragraph prior to the current paragraph 13 as part of
the description of shared_future::wait()
:
waitable()
returns true
.Add a new paragraph prior to the current paragraph 16 as part of
the description of shared_future::wait_for()
:
waitable()
returns true
.Add a new paragraph prior to the current paragraph 19 as part of
the description of shared_future::wait_until()
:
waitable()
returns true
.I am grateful to Alisdair Meredith and Jonathan Wakely for their comments on drafts of this paper.