I recently realized that the addition of move semantics in C++11 (or at least my implementation of it, Visual C++) has actively (and quite dramatically) broken one of my optimizations.
Consider the following code:
#include <vector>
int main()
{
typedef std::vector<std::vector<int> > LookupTable;
LookupTable values(100); // make a new table
values[0].push_back(1); // populate some entries
// Now clear the table but keep its buffers allocated for later use
values = LookupTable(values.size());
return values[0].capacity();
}
I followed this kind of pattern to perform container recycling: I would re-use the same container instead of destroying and recreating it, to avoid unnecessary heap deallocation and (immediate) reallocation.
On C++03, this worked fine -- that means this code used to return 1, because the vectors were copied elementwise, while their underlying buffers were kept as-is. Consequently I could modify each inner vector knowing that it could use the same buffer as it had before.
On C++11, however, I noticed that this results in a move of the right-hand side onto the left-hand side, which performs an element-wise move-assignment to each vector on the left-hand side. This in turn causes the vector to discard its old buffer, suddenly reducing its capacity to zero. Consequently, my application now slows down considerably due to excess heap allocations/deallocations.
My question is: is this behavior a bug, or is it intentional? Is it even specified by the standard at all?
Update:
I just realized that correctness of this particular behavior may depend on whether or not a = A() can invalidate iterators that point to the elements of a. However, I don't know what the iterator invalidation rules for move-assignment are, so if you're aware of them it may be worth mentioning those in your answer.
C++11
The difference in the behaviours in the OP between C++03 and C++11 are due to the way move assignment is implemented.
There are two main options:
Destroy all elements of the LHS. Deallocate the LHS's underlying storage. Move the underlying buffer (the pointers) from the RHS to the LHS.
Move-assign from the elements of the RHS to the elements of the LHS. Destroy any excess elements of the LHS or move-construct new elements in the LHS if the RHS has more.
I think it is possible to use option 2 with copies, if moving is not noexcept.
Option 1 invalidates all references/pointers/iterators to the LHS, and preserves all iterators etc. of the RHS. It needs O(LHS.size()) destructions, but the buffer movement itself is O(1).
Option 2 invalidates only iterators to excess elements of the LHS which are destroyed, or all iterators if a reallocation of the LHS occurs. It is O(LHS.size() + RHS.size()) since all elements of both sides need to be taken care of (copied or destroyed).
As far as I can tell, there is no guarantee which one happens in C++11 (see next section).
In theory, you can use option 1 whenever you can deallocate the underlying buffer with the allocator that is stored in the LHS after the operation. This can be achieved in two ways:
If two allocators compare equal, one can be used to deallocate the storage allocated via the other one. Therefore, if the allocators of LHS and RHS compare equal before the move, you can use option 1. This is a run-time decision.
If the allocator can be propagated (moved or copied) from the RHS to the LHS, this new allocator in the LHS can be used to deallocate the storage of the RHS. Whether or not an allocator is propagated is determined by allocator_traits<your_allocator :: propagate_on_container_move_assignment. This is decided by type properties, i.e. a compile-time decision.
C++11 minus defects / C++1y
After LWG 2321 (which is still open), we have the guarantee that:
no move constructor (or move assignment operator when
allocator_traits<allocator_type> :: propagate_on_container_move_assignment :: value is true) of a container (except for array) invalidates any references,
pointers, or iterators referring to the elements of the source
container. [ Note: The end() iterator does not refer to any element, so
it may be invalidated. — end note ]
This requires that move-assignment for those allocators which are propagated on move assignment has to move the pointers of the vector object, but must not move the elements of the vector. (option 1)
The default allocator, after LWG defect 2103, is propagated during move-assignment of the container, hence the trick in the OP is forbidden to move the individual elements.
My question is: is this behavior a bug, or is it intentional? Is it even specified by the standard at all?
No, yes, no (arguably).
See this answer for a detailed description of how vector move assignment must work. As you are using the std::allocator, C++11 puts you into case 2, which many on the committee considered a defect, and has been corrected to case 1 for C++14.
Both case 1 and case 2 have identical run time behavior, but case 2 has additional compile-time requirements on the vector::value_type. Both case 1 and case 2 result in memory ownership being transferred from the rhs to the lhs during the move assignment, giving you the results you observe.
This is not a bug. It is intentional. It is specified by C++11 and forward. Yes, there are some minor defects as dyp pointed out in his answer. But none of these defects will change the behavior you are seeing.
As was pointed out in the comments, the simplest fix for you is to create an as_lvalue helper and use that:
template <class T>
constexpr
inline
T const&
as_lvalue(T&& t)
{
return t;
}
// ...
// Now clear the table but keep its buffers allocated for later use
values = as_lvalue(LookupTable(values.size()));
This is zero-cost, and returns you to precisely the C++03 behavior. It might not pass code review though. It would be clearer for you to iterate through and clear each element in the outer vector:
// Now clear the table but keep its buffers allocated for later use
for (auto& v : values)
v.clear();
The latter is what I recommend. The former is (imho) obfuscated.
Related
I've got following code:
std::list some_data;
...
std::list new_data = std::move(some_data);
some_data.clear();
...
The question is whether some_data.clear() is necessary? (for the record, some_data will be reused in the future)
Yes, it's necessary.
Only the std smart pointers are guaranteed to be in a default constructed state after being moved from.
Containers are in an valid, but unspecified state. This means you can only call member functions without preconditions, e.g. clear, that put the object in a fully known state.
The working draft of standard (N4713) states about the state of objects after they are moved from:
20.5.5.15 Moved-from state of library types [lib.types.movedfrom]
1 Objects of types defined in the C++ standard library may be moved from. Move operations may be explicitly specified or implicitly generated. Unless otherwise specified, such moved-from objects shall be placed in a valid but unspecified state.
20.3.25 [defns.valid]
valid but unspecified state value of an object that is not specified except that the object’s invariants are met and operations on the object behave as specified for its type
The only operations you can safely perform on a moved-from container are those that do not require a precondition. clear has no preconditions. And it will return the object to a known state from which it can be used again.
No it is not necessary to clear the list. It is just reasonable and convenient to do so.
The list will have N elements, each taking an unspecified value, for some N≥0. So if you want to re-poppulate the list, you may in theory assign to the elements that are kept rather than clear and re-insert everything from scratch.
Of course chances that N≠0 are vanishingly small, so in practice clearing is the right choice.
Short answer:
Yes, you should clear it, because the standard doesn't explicitly define in what state the source list is after the move.
Furthermore, always calling clear() when a container gets reused after being moved from is a simple rule that should not cause any significant overhead even when it is redundant.
Longer answer:
As I mentioned in some comments, I can't imagine any legal and sensible implementation for which the source std::list will be anything but an empty std::list after being used as the source for a move constructor (thanks #Lightness Races in Orbit for digging through the standard). Move constructing must happen in linear time, which doesn't allow any per-object operations and AFAIK it is also not allowed to have a small, fixed-size inplace buffer that could be a source of left-over "zombie" elements in the source list.
However, even if you can track down all of that in the standard, you'd still have to document that everytime you omit the clear() in the code and be wary of someone refactoring the code by replacing the std::list with a homegrown container, that doesn't quite full fill the requirements in the standard (e.g. to make use of the small buffer optimization). Also, the previous statement is only valid for move construction. On moveassignment it is absolutely possible that the moved from container will not be empty.
In summary: Even if it was technically correct to not use clear in this particular case, it is imho not worth the mental effort to do it (and if you are in an environment where it is worth it, you are probably tuning your code to a particular version of the standard library and not the standard document anyway).
It rather depends on what you mean by "reused". There are many reuses that totally define the state of the object, with no regard to it's previous state.
The obvious one is assigning to a moved-from object, which occurs in the naive swap
template <typename T>
void swap(T& lhs, T& rhs)
{
T temp = std::move(lhs);
lhs = std::move(rhs); // lhs is moved-from, but we don't care.
rhs = std::move(temp); // rhs is moved-from, but we don't care.
}
If you instead wish to "reuse" by calling some_data.push_back, then yes, you should clear it first
If I have an iterator into vector a, then I move-construct or move-assign vector b from a, does that iterator still point to the same element (now in vector b)? Here's what I mean in code:
#include <vector>
#include <iostream>
int main(int argc, char *argv[])
{
std::vector<int>::iterator a_iter;
std::vector<int> b;
{
std::vector<int> a{1, 2, 3, 4, 5};
a_iter = a.begin() + 2;
b = std::move(a);
}
std::cout << *a_iter << std::endl; // Is a_iter valid here?
return 0;
}
Is a_iter still valid since a has been moved into b, or is the iterator invalidated by the move? For reference, std::vector::swap does not invalidate iterators.
While it might be reasonable to assume that iterators are still valid after a move, I don't think the Standard actually guarantees this. Therefore, the iterators are in an undefined state after the move.
There is no reference I can find in the Standard which specifically states that iterators that existed before a move are still valid after the move.
On the surface, it would seem to be perfectly reasonable to assume that an iterator is typically implemented as pointers in to the controlled sequence. If that's the case, then the iterators would still be valid after the move.
But the implementation of an iterator is implementation-defined. Meaning, so long as the iterator on a particular platform meets the requirements set forth by the Standard, it can be implemented in any way whatsoever. It could, in theory, be implemented as a combination of a pointer back to the vector class along with an index. If that's the case, then the iterators would become invalid after the move.
Whether or not an iterator is actually implemented this way is irrelevant. It could be implemented this way, so without a specific guarantee from the Standard that post-move iterators are still valid, you cannot assume that they are. Bear in mind also that there is such a guarantee for iterators after a swap. This was specifically clarified from the previous Standard. Perhaps it was simply an oversight of the Std comittee to not make a similar clarification for iterators after a move, but in any case there is no such guarantee.
Therefore, the long and the short of it is you can't assume your iterators are still good after a move.
EDIT:
23.2.1/11 in Draft n3242 states that:
Unless otherwise specified (either explicitly or by defining a
function in terms of other functions), invoking a container member
function or passing a container as an argument to a library function
shall not invalidate iterators to, or change the values of, objects
within that container.
This might lead one to conclude that the iterators are valid after a move, but I disagree. In your example code, a_iter was an iterator in to the vector a. After the move, that container, a has certainly been changed. My conclusion is the above clause does not apply in this case.
I think the edit that changed move construction to move assignment changes the answer.
At least if I'm reading table 96 correctly, the complexity for move construction is given as "note B", which is constant complexity for anything except std::array. The complexity for move assignment, however, is given as linear.
As such, the move construction has essentially no choice but to copy the pointer from the source, in which case it's hard to see how the iterators could become invalid.
For move assignment, however, the linear complexity means it could choose to move individual elements from the source to the destination, in which case the iterators will almost certainly become invalid.
The possibility of move assignment of elements is reinforced by the description: "All existing elements of a are either move assigned to or destroyed". The "destroyed" part would correspond to destroying the existing contents, and "stealing" the pointer from the source -- but the "move assigned to" would indicate moving individual elements from source to destination instead.
tl;dr : Yes, moving a std::vector<T, A> possibly invalidates the iterators
The common case (with std::allocator in place) is that invalidation does not happen but there is no guarantee and switching compilers or even the next compiler update might make your code behave incorrect if you rely on the fact the your implementation currently does not invalidate the iterators.
On move assignment:
The question whether std::vector iterators can actually remain valid after move-assignment is connected with the allocator awareness of the vector template and depends on the allocator type (and possibly the respective instances thereof).
In every implementation I have seen, move-assignment of a std::vector<T, std::allocator<T>>1 will not actually invalidate iterators or pointers. There is a problem however, when it comes down to making use of this, as the standard just cannot guarantee that iterators remain valid for any move-assignment of a std::vector instance in general, because the container is allocator aware.
Custom allocators may have state and if they do not propagate on move assignment and do not compare equal, the vector must allocate storage for the moved elements using its own allocator.
Let:
std::vector<T, A> a{/*...*/};
std::vector<T, A> b;
b = std::move(a);
Now if
std::allocator_traits<A>::propagate_on_container_move_assignment::value == false &&
std::allocator_traits<A>::is_always_equal::value == false && (possibly as of c++17)
a.get_allocator() != b.get_allocator()
then b will allocate new storage and move elements of a one by one into that storage, thus invalidating all iterators, pointers and references.
The reason is that fulfillment of above condition 1. forbidds move assignment of the allocator on container move. Therefore, we have to deal with two different instances of the allocator. If those two allocator objects now neither always compare equal (2.) nor actually compare equal, then both allocators have a different state. An allocator x may not be able to deallocate memory of another allocator y having a different state and therefore a container with allocator x cannot just steal memory from a container which allocated its memory via y.
If the allocator propagates on move assignment or if both allocators compare equal, then an implementation will very likely choose to just make b own as data because it can be sure to be able to deallocate the storage properly.
1: std::allocator_traits<std::allocator<T>>::propagate_on_container_move_assignment and std::allocator_traits<std::allocator<T>>::is_always_equal both are typdefs for std::true_type (for any non-specialized std::allocator).
On move construction:
std::vector<T, A> a{/*...*/};
std::vector<T, A> b(std::move(a));
The move constructor of an allocator aware container will move-construct its allocator instance from the allocator instance of the container which the current expression is moving from. Thus, the proper deallocation capability is ensured and the memory can (and in fact will) be stolen because move construction is (except for std::array) bound to have constant complexity.
Note: There is still no guarantee for iterators to remain valid even for move construction.
On swap:
Demanding the iterators of two vectors to remain valid after a swap (now just pointing into the respective swapped container) is easy because swapping only has defined behaviour if
std::allocator_traits<A>::propagate_on_container_swap::value == true ||
a.get_allocator() == b.get_allocator()
Thus, if the allocators do not propagate on swap and if they do not compare equal, swapping the containers is undefined behaviour in the first place.
Since there's nothing to keep an iterator from keeping a reference or pointer to the original container, I'd say you can't rely on the iterators remaining valid unless you find an explicit guarantee in the standard.
I just stumbled into an issue with std::vector when adding new elements to it.
It seems when you try to add more elements to it, and it needs to allocate more space, it does so by copying last element all elements it currently holds. This seems to assume that any element in the vector is fully valid, and as such a copy will always succeed.
In our case, this is not necessarily true. Currently we might have some left over elements in the vector, because we chose not to remove them yet, which are valid objects, but their data doesn't guarantee valid behavior. The objects have guards, but I never considered adding guards to a copy constructor as I assumed we would never be copying an invalid object (which vector forces) :
CopiedClass::CopiedClass(const CopiedClass& other)
: member(other.member)
{
member->DoSomething();
}
It just so happened that "member" is nulled when we are done with the original object and leave it lying about in the vector, so when std::vector tries to copy it, it crashes.
Is it possible to prevent std::vector from copying that element? Or do we have to guard against possible invalid objects being copied? Ideally we'd like to keep assuming that only valid objects are created, but that seems to imply we immediately clean them from the vector, rather than waiting and doing it at some later stage.
It's actually a design flaw in your CopiedClass that needs to be fixed.
std::vector behaves as expected and documented.
From the little code you show
member->DoSomething();
this indicates you are going to take a shallow copy of a pointer to what ever.
in our case, this is not necessarily true. Currently we might have some left over elements in the vector, because we chose not to remove them yet, which are valid objects, but their data doesn't guarantee valid behavior.
It just so happened that "member" is nulled when we are done with the original object and leave it lying about in the vector, so when std::vector tries to copy it, it crashes.
As your copy constructor doesn't handle that situation correctly regarding following the Rule of Three your design of CopiedClass is wrong.
You should either care to create a deep copy of your member, or use a smart pointer (or a plain instance member), rather than a raw pointer.
The smart pointers should take care of the management for these members correctly.
Also for the code snippet from above, you should test against nullpointer before blindly dereferencing and calling DoSomething().
Is it possible to prevent std::vector from copying that element?
As you're asking for c++11 it's possible, but requires you never change the size of the vector, and provide a move constructor and assignment operator for CopiedClass.
Otherwise, std::vector explicitly requires copyable types (at least for certain operations):
T must meet the requirements of CopyAssignable and CopyConstructible.
It's your responsibility to meet these requirements correctly.
... it does so by copying the last element it currently holds.
Also note: In a situation the vector needs to be resized, all of the existing elements will be copied, not only the last one as you premise.
If you know the vector's maximum size in advance, then a workaround for this issue would be calling reserve with that maximum size. When the vector's capacity is big enough, no reallocations occur and no copies of existing elements need to be made.
But that's really just a workaround. You should redesign your class such that copying cannot suddenly become an invalid operation, either by making copying safe or by disallowing it and perhaps replace it with moving, like std::unique_ptr. If you make the validity of copying dependent on some run-time status, then you don't even need a std::vector to get into trouble. A simple CopiedClass get(); will be a potential error.
It seems when you try to add more elements to it, and it needs to
allocate more space, it does so by copying the last element it
currently holds. This seems to assume that any element in the vector
is fully valid, and as such a copy will always succeed.
Both sentences are not really correct. When vector runs out of space it, yes, performs reallocation, but of all the elements and these are copied to the new location or possibly moved. When you push_back or emplace_back, and reallocation is needed, the element will generally by copied: if an exception is thrown during that, it will be treated as if you had never added the element. If vector detects a noexcept user-defined move constructor ( MoveInsertable concept), elements are moved; if this constructor though is not noexcept, and an exception is thrown, the result is unspecified.
The objects have guards, but I never considered adding guards to a
copy constructor as I assumed we would never be copying an invalid
object (which vector forces) :
vector copies its value_type: it does not care whether what it contains is valid or invalid. You should take care of that in your copy control methods, whose scope is exactly to define how the object is passed around.
CopiedClass::CopiedClass(const CopiedClass& other)
: member(other.member)
{
member->DoSomething();
}
The obvious solution would be to check if member is valid and act thereby.
if (member.internalData.isValid())
member->DoSomething()
// acknowledge this.member's data is invalid
We don't know how member is represented, but Boost.Optional is what you may be looking for.
Is it possible to prevent std::vector from copying that element?
Reallocation is something vector is expected to commit so, no, you can't. reserv-ing space could avoid that, but maintaining code to handle that would not really be painless. Rather, prefer a container like std::forward_list/std::list.
Other solutions:
Hold a unique_ptr<decltype(member)>, but pointers are not often a real solution
Define move semantics for your object, as described in other answers.
What are the reasons behind the change in std::vector::resize from the pre-C++11:
void resize( size_type count, T value = T() );
to the compatible C++11 form:
void resize( size_type count );
void resize( size_type count, const value_type& value);
Paragraph C.2.12 of the Annex C (Compatibility) to the C++11 Standard specifies:
Change: Signature changes: resize
Rationale: Performance, compatibility with move semantics.
Effect on original feature: For vector, deque, and list the fill value passed to resize is now passed by
reference instead of by value, and an additional overload of resize has been added. Valid C++ 2003 code
that uses this function may fail to compile with this International Standard.
The old resize() function was copy-constructing new elements from value. This makes it impossible to use resize() when the elements of the vector are default-constructible but non-copyable (you may want to move-assign them later on). This explains the "Compatibility with move semantics" rationale.
Moreover, it may be slow if you do not want any copy to occur, just new elements to be default-constructed. Also, the value parameter is passed by value in the C++03 version, which incurs in the overhead of an unnecessary copy (as mentioned by TemplateRex in his answer). This explains the "Performance" rationale.
One reason is that default arguments are always passed, i.e. copied in this case. Doing
my_vector.resize(1000000)
would copy 1 million T objects.
In C++11 you now have a choice between copying a user-provided value or default-inserting (i.e. constructing) elements in-place, using the std::allocator_traits<Alloc>::construct() function. This allows resizing of vector with elements that are CopyInsertable but not Copyable.
Note that this change has been done to all sequence containers having a resize() member (vector, deque, forward_list and list), but not for std::string which didn't have a default value argument to begin with.
Update: apart from the Annex to the current Standard cited by #AndyProwl, the original defect report by #HowardHinnant also clarifies:
The problem with passing T by value is that it can be significantly
more expensive than passing by reference. The converse is also true,
however when it is true it is usually far less dramatic (e.g. for
scalar types).
Even with move semantics available, passing this parameter by value
can be expensive. Consider for example vector>:
std::vector<int> x(1000); std::vector<std::vector<int>> v; ...
v.resize(v.size()+1, x);
In the pass-by-value case, x is copied once
to the parameter of resize. And then internally, since the code can
not know at compile time by how much resize is growing the vector, x
is usually copied (not moved) a second time from resize's parameter
into its proper place within the vector.
With pass-by-const-reference, the x in the above example need be
copied only once. In this case, x has an expensive copy constructor
and so any copies that can be saved represents a significant savings.
If we can be efficient for push_back, we should be efficient for
resize as well. The resize taking a reference parameter has been coded
and shipped in the CodeWarrior library with no reports of problems
which I am aware of.
If I have an iterator into vector a, then I move-construct or move-assign vector b from a, does that iterator still point to the same element (now in vector b)? Here's what I mean in code:
#include <vector>
#include <iostream>
int main(int argc, char *argv[])
{
std::vector<int>::iterator a_iter;
std::vector<int> b;
{
std::vector<int> a{1, 2, 3, 4, 5};
a_iter = a.begin() + 2;
b = std::move(a);
}
std::cout << *a_iter << std::endl; // Is a_iter valid here?
return 0;
}
Is a_iter still valid since a has been moved into b, or is the iterator invalidated by the move? For reference, std::vector::swap does not invalidate iterators.
While it might be reasonable to assume that iterators are still valid after a move, I don't think the Standard actually guarantees this. Therefore, the iterators are in an undefined state after the move.
There is no reference I can find in the Standard which specifically states that iterators that existed before a move are still valid after the move.
On the surface, it would seem to be perfectly reasonable to assume that an iterator is typically implemented as pointers in to the controlled sequence. If that's the case, then the iterators would still be valid after the move.
But the implementation of an iterator is implementation-defined. Meaning, so long as the iterator on a particular platform meets the requirements set forth by the Standard, it can be implemented in any way whatsoever. It could, in theory, be implemented as a combination of a pointer back to the vector class along with an index. If that's the case, then the iterators would become invalid after the move.
Whether or not an iterator is actually implemented this way is irrelevant. It could be implemented this way, so without a specific guarantee from the Standard that post-move iterators are still valid, you cannot assume that they are. Bear in mind also that there is such a guarantee for iterators after a swap. This was specifically clarified from the previous Standard. Perhaps it was simply an oversight of the Std comittee to not make a similar clarification for iterators after a move, but in any case there is no such guarantee.
Therefore, the long and the short of it is you can't assume your iterators are still good after a move.
EDIT:
23.2.1/11 in Draft n3242 states that:
Unless otherwise specified (either explicitly or by defining a
function in terms of other functions), invoking a container member
function or passing a container as an argument to a library function
shall not invalidate iterators to, or change the values of, objects
within that container.
This might lead one to conclude that the iterators are valid after a move, but I disagree. In your example code, a_iter was an iterator in to the vector a. After the move, that container, a has certainly been changed. My conclusion is the above clause does not apply in this case.
I think the edit that changed move construction to move assignment changes the answer.
At least if I'm reading table 96 correctly, the complexity for move construction is given as "note B", which is constant complexity for anything except std::array. The complexity for move assignment, however, is given as linear.
As such, the move construction has essentially no choice but to copy the pointer from the source, in which case it's hard to see how the iterators could become invalid.
For move assignment, however, the linear complexity means it could choose to move individual elements from the source to the destination, in which case the iterators will almost certainly become invalid.
The possibility of move assignment of elements is reinforced by the description: "All existing elements of a are either move assigned to or destroyed". The "destroyed" part would correspond to destroying the existing contents, and "stealing" the pointer from the source -- but the "move assigned to" would indicate moving individual elements from source to destination instead.
tl;dr : Yes, moving a std::vector<T, A> possibly invalidates the iterators
The common case (with std::allocator in place) is that invalidation does not happen but there is no guarantee and switching compilers or even the next compiler update might make your code behave incorrect if you rely on the fact the your implementation currently does not invalidate the iterators.
On move assignment:
The question whether std::vector iterators can actually remain valid after move-assignment is connected with the allocator awareness of the vector template and depends on the allocator type (and possibly the respective instances thereof).
In every implementation I have seen, move-assignment of a std::vector<T, std::allocator<T>>1 will not actually invalidate iterators or pointers. There is a problem however, when it comes down to making use of this, as the standard just cannot guarantee that iterators remain valid for any move-assignment of a std::vector instance in general, because the container is allocator aware.
Custom allocators may have state and if they do not propagate on move assignment and do not compare equal, the vector must allocate storage for the moved elements using its own allocator.
Let:
std::vector<T, A> a{/*...*/};
std::vector<T, A> b;
b = std::move(a);
Now if
std::allocator_traits<A>::propagate_on_container_move_assignment::value == false &&
std::allocator_traits<A>::is_always_equal::value == false && (possibly as of c++17)
a.get_allocator() != b.get_allocator()
then b will allocate new storage and move elements of a one by one into that storage, thus invalidating all iterators, pointers and references.
The reason is that fulfillment of above condition 1. forbidds move assignment of the allocator on container move. Therefore, we have to deal with two different instances of the allocator. If those two allocator objects now neither always compare equal (2.) nor actually compare equal, then both allocators have a different state. An allocator x may not be able to deallocate memory of another allocator y having a different state and therefore a container with allocator x cannot just steal memory from a container which allocated its memory via y.
If the allocator propagates on move assignment or if both allocators compare equal, then an implementation will very likely choose to just make b own as data because it can be sure to be able to deallocate the storage properly.
1: std::allocator_traits<std::allocator<T>>::propagate_on_container_move_assignment and std::allocator_traits<std::allocator<T>>::is_always_equal both are typdefs for std::true_type (for any non-specialized std::allocator).
On move construction:
std::vector<T, A> a{/*...*/};
std::vector<T, A> b(std::move(a));
The move constructor of an allocator aware container will move-construct its allocator instance from the allocator instance of the container which the current expression is moving from. Thus, the proper deallocation capability is ensured and the memory can (and in fact will) be stolen because move construction is (except for std::array) bound to have constant complexity.
Note: There is still no guarantee for iterators to remain valid even for move construction.
On swap:
Demanding the iterators of two vectors to remain valid after a swap (now just pointing into the respective swapped container) is easy because swapping only has defined behaviour if
std::allocator_traits<A>::propagate_on_container_swap::value == true ||
a.get_allocator() == b.get_allocator()
Thus, if the allocators do not propagate on swap and if they do not compare equal, swapping the containers is undefined behaviour in the first place.
Since there's nothing to keep an iterator from keeping a reference or pointer to the original container, I'd say you can't rely on the iterators remaining valid unless you find an explicit guarantee in the standard.