76

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.

David Brown
  • 13,336
  • 4
  • 38
  • 55
  • Well the whole point of moving it is that you **don't** use it after. I can't say for sure whether it is or not, though. – chris Jun 13 '12 at 19:15
  • 1
    @chris I am hoping that `a_iter` now references an element in `b` after `a` is moved. – David Brown Jun 13 '12 at 19:21
  • 6
    Pedantic -- you didn't move-construct, you move-assigned. – John Dibling Jun 13 '12 at 19:21
  • My initial knee-jerk reaction to this question is the iterator is not valid or is in an undefined state, but I cannot find a reference in the Standard. – John Dibling Jun 13 '12 at 19:25
  • @JohnDibling edited :). My example originally was using move construction, but then I added the inner scope so `a` would go out of scope and switched to assignment at the same time. – David Brown Jun 13 '12 at 19:25
  • Why don't you write a little program to test that? It shouldn't be difficult. – Thomash Jun 13 '12 at 19:36
  • 1
    @Thomash: I don't think hacking together a sample will be sufficient in this case. – John Dibling Jun 13 '12 at 19:39
  • 8
    @Thomash: If the answer is that it *does* invalidate iterators, then it's undefined behavior to dereference them, so how would you test it? – Benjamin Lindley Jun 13 '12 at 19:41
  • 1
    I can't think of a reason why iterators would be invalidated, but I can't find any quotes in the standard to support that... Since the validity of iterators after a swap is well-defined, it seems reasonable to think that the same reasoning can apply when moving (even more if we think about how `vectors` are implemented). – Luc Touraille Jun 13 '12 at 19:47
  • 1
    @Luc: Iterators could be invalidated if the iterator class itself maintained pointers back in to the vector class. Just spitballing. – John Dibling Jun 13 '12 at 19:50
  • 1
    @JohnDibling: Of course, I should have said "I can't think of a reasonable reason why the committee would have allowed iterators to be invalidated" :). And good luck for correctly implementing `swap` with iterators keeping a reference to their containers :)! – Luc Touraille Jun 13 '12 at 19:56

4 Answers4

29

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.

Sven Marnach
  • 574,206
  • 118
  • 941
  • 841
John Dibling
  • 99,718
  • 31
  • 186
  • 324
  • 1
    +1 But, maybe you can reasonably assume they _are_ still good after a move - but just know that it may not work if you switch compilers. It works in every compiler I just tested, and it probably always will. – David Jun 13 '12 at 20:04
  • 7
    @Dave: Reliance on Undefined Behavior is a very slippery slope and, pedantically, technically invalid. You're best off just not doing it. – John Dibling Jun 13 '12 at 20:04
  • 3
    I would normally agree, but it would be hard to write a swap that maintains valid iterators and a move assignment which does not. It would almost take intentional effort by the library writer to have the iterators invalidated. Besides, undefined is my favorite kind of behavior. – David Jun 13 '12 at 20:08
  • @Sven Marnach: I never know Weather to use Weather or Whether :) – John Dibling Jun 15 '12 at 14:36
  • 7
    This is [LWG 2321](http://www.open-std.org/JTC1/SC22/WG21/docs/lwg-active.html#2321) – dyp Jun 07 '15 at 01:25
  • 1
    Regarding "_After the `move`, that container, `a` has certainly been changed. My conclusion is the above clause does not apply in this case._" - Couldn't the same argument be made for `swap`? It changes both containers. Yet, if we do `std::swap(a, b);`, valid iterators to elements in `a` are now guaranteed to be valid iterators to elements in `b` and vice versa. – Ted Lyngmo Jun 17 '20 at 08:48
12

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.

Jerry Coffin
  • 476,176
  • 80
  • 629
  • 1,111
  • I see the same thing as you in table 96, but I'm shocked move construction and move assignment have different complexity requirements! Does a conforming implementation _have_ to match the complexity in that table, or can it do better? (AKA: is a std::vector that copies the pointer to it's data in the move assignment op standard conformant?) – David Jun 13 '12 at 19:49
  • @Dave: A comforming implementation must do no worse than any performance guarantees dictated in the Standard. – John Dibling Jun 13 '12 at 19:51
  • 10
    I actually think it is linear in terms of the size of the container being assigned to. This is the same as the destructor being linear, it has to destroy all existing items, a problem the move-constructor does not have as there are no existing items. – KillianDS Jun 13 '12 at 19:59
  • Why wouldn't they require constant complexity for std::vector move assignment?! (and all other containers...) – David Jun 13 '12 at 19:59
  • 1
    @Dave: Consider assigning a container of 2 elements to one that had 50. One way or another, the extra 48 elements have to be destroyed. It can either destroy *all* the elements in the destination, then move the pointer to the source buffer into the destination container, or else it can move the 2 elements to the destination, and destroy the other 48. Either way it needs to be linear though. – Jerry Coffin Jun 13 '12 at 20:53
  • 4
    It's not just linear in the number of elements to be destroyed. As stated in [container.requirements.general]/7 move construction always moves the allocator, move assignment only moves the allocator if `propagate_on_container_move_assignment` is true, if it's not true and the allocators are not equal then the existing storage cannot be moved and so there's a possible reallocation and each element is moved individually. – Jonathan Wakely Dec 28 '12 at 16:17
5

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

  1. std::allocator_traits<A>::propagate_on_container_move_assignment::value == false &&
  2. std::allocator_traits<A>::is_always_equal::value == false && (possibly as of c++17)
  3. 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

  1. std::allocator_traits<A>::propagate_on_container_swap::value == true ||
  2. 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.

Pixelchemist
  • 24,090
  • 7
  • 47
  • 71
4

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.

Mark Ransom
  • 299,747
  • 42
  • 398
  • 622
  • +1: I would agree with this assesment, and for what it's worth I've been looking for such a reference for the last 30 minutes and I can't find anything. :) – John Dibling Jun 13 '12 at 19:50
  • 3
    Agreed with John Dibling, the closest reference is that iterators are not invalidated if two containers are swapped, which seems to indicate that it should be valid, but I did not find any such guarantee. It is surprising how silent the standard regarding move of containers. – David Rodríguez - dribeas Jun 13 '12 at 19:55
  • 1
    Is it not the other way around? Can't you assume that iterators remain valid unless otherwise specified by the standard? This is what I understand from this quote: `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.` [container.requirements.general] – Luc Touraille Jun 13 '12 at 20:05
  • @Luc: You should post that as an answer before I steal it for myself. :) – John Dibling Jun 13 '12 at 20:08
  • @JohnDibling: Please, feel free to update your answer with this :). – Luc Touraille Jun 13 '12 at 20:10
  • @Luc: But moving from a container certainly *does* change the values of the container, from having them to not having them. So, I would presume that iterators can also be invalidated. – Benjamin Lindley Jun 13 '12 at 20:15
  • 4
    Wouldn't `vector::swap` being constant time and also not invalidating iterators prevent `vector::iterator` from containing a pointer to the original container? – David Brown Jun 13 '12 at 20:20
  • @BenjaminLindley: This is changing the container itself, not the objects contained. However, as noted by Jerry, in the case of move assignment, the objects contained by the source container can be move assigned from, meaning that their value can be changed (whether or not this is clearly specified in the standard is debatable). Anyway, the fact that the values of contained objects can be changed does not imply that iterators will be invalidated, does it? – Luc Touraille Jun 13 '12 at 20:24
  • 5
    @LucTouraille: I don't know. I'm only allowed to comprehend so much standardese per month, and I've exceeded my limit. – Benjamin Lindley Jun 13 '12 at 20:26
  • @Benjamin : But it's not even halfway through! ;-] – ildjarn Jun 13 '12 at 22:02