LWG-2839 Self-move-assignment of library types, again
LWG-2839 "Self-move-assignment of library types, again" clarifies that move-assigning most Standard Library types to themselves leaves the object in a valid-but-unspecified state. While we are fairly fastidious about handling self-move and self-swap correctly when authoring new types, I'm not sure that our predecessors paid as much attention. We need to audit all of the move assignment operators in the STL to ensure that self-move leaves the class in a valid state except when the standard explicitly specifies otherwise.
Headers to Audit
- [ ]
<algorithm> - [ ]
<any> - [ ]
<array> - [ ]
<atomic> - [ ]
<barrier> - [ ]
<bit> - [ ]
<bitset> - [ ]
<charconv> - [ ]
<chrono> - [ ]
<codecvt> - [ ]
<compare> - [ ]
<complex> - [ ]
<concepts> - [ ]
<condition_variable> - [ ]
<coroutine> - [ ]
<deque> - [ ]
<exception> - [ ]
<execution> - [ ]
<expected> - [ ]
<filesystem> - [ ]
<flat_map>(not yet implemented inmain) - [ ]
<flat_set>(not yet implemented inmain) - [ ]
<format> - [ ]
<forward_list> - [ ]
<fstream> - [ ]
<functional> - [ ]
<future> - [ ]
<generator>(not yet implemented inmain) - [ ]
<initializer_list> - [ ]
<iomanip> - [ ]
<ios> - [ ]
<iosfwd> - [ ]
<iostream> - [ ]
<istream> - [ ]
<iterator> - [ ]
<latch> - [ ]
<limits> - [ ]
<list> - [ ]
<locale> - [ ]
<map> - [ ]
<mdspan> - [ ]
<memory> - [ ]
<memory_resource> - [ ]
<mutex> - [ ]
<new> - [ ]
<numbers> - [ ]
<numeric> - [ ]
<optional> - [ ]
<ostream> - [ ]
<print> - [ ]
<queue> - [ ]
<random> - [ ]
<ranges> - [ ]
<ratio> - [ ]
<regex> - [ ]
<scoped_allocator> - [ ]
<semaphore> - [ ]
<set> - [ ]
<shared_mutex> - [ ]
<source_location> - [ ]
<span> - [ ]
<spanstream> - [ ]
<sstream> - [ ]
<stack> - [ ]
<stacktrace> - [ ]
<stdexcept> - [ ]
<stdfloat> - [ ]
<stop_token> - [ ]
<streambuf> - [ ]
<string> - [ ]
<string_view> - [ ]
<strstream> - [ ]
<syncstream> - [ ]
<system_error> - [ ]
<thread> - [ ]
<tuple> - [ ]
<type_traits> - [ ]
<typeindex> - [ ]
<typeinfo> - [ ]
<unordered_map> - [ ]
<unordered_set> - [ ]
<utility> - [ ]
<valarray> - [ ]
<variant> - [ ]
<vector> - [ ]
<version>
To make this more actionable, I've added a list of all of the C++23 headers, so we can audit them one-by-one. It may be convenient to use _EXPORT_STD to find the public classes defined by each header.
Because I'm a lazy kitty, I haven't yet bothered checking off the headers that obviously define no types (like <version>).
Is this a good move-assignment implementation? https://github.com/microsoft/STL/blob/5762e6bcaf7f5f8b5dba0a9aabf0acbd0e335e80/stl/inc/barrier#L55-L59
I'm not sure. On one hand it leaves arrival token in valid but unspecified state. On the other hand the token information is lost.
Looks good enough to me. Valid-but-unspecified means exactly what it says on the box - it doesn't say anything about being a no-op.
I see some strange (and seems intended) behavior when self-move-assigning _Node_handle:
https://github.com/microsoft/STL/blob/2e27f1f05a155bb3637dad04b4d9c4c4590c8542/stl/inc/xnode_handle.h#L128-L131
This, while following what standard said, and does leave the object in a valid-but-unspecified state, but also clears the node and lost the data it holds when self-move-assigning.
This behavior is kind of surprising and does not align with most of other stl types do, they generally do no-op when self-move-assigning, like std::string/vector/....
My questions are:
_Node_handleclears itself when self-move-assigning. Is this behavior mandatory and cannot being just no-op because what the standard said about its implementation?- Most of msvc-stl types is implemented doing no-op when self-move-assigning. Is this behavior not guaranteed by standard, not encouraged by standard, and should not be relied on when using them?
_Node_handleclears itself when self-move-assigning. Is this behavior mandatory and cannot being just no-op because what the standard said about its implementation?
Yes. This is "otherwise specified" in [container.node.cons]/3.4.
- Most of msvc-stl types is implemented doing no-op when self-move-assigning. Is this behavior not guaranteed by standard, not encouraged by standard, and should not be relied on when using them?
A few of no-op treatments are standard-guaranteed, e.g. [range.move.wrap] and [thread.jthread.cons]/12. The rest are not.
I don't think we should rely on the resulted value after self-move-assignment, even though sometimes no-op is considered to be the most reasonable choice and standardized. I think we can rely on the validity of the self-move-assignment operation itself and the result of subsequent (non-self) assignment to that object, though.