36

§23.1.2.8 in the standard states that insertion/deletion operations on a set/map will not invalidate any iterators to those objects (except iterators pointing to a deleted element).

Now, consider the following situation: you want to implement a graph with uniquely numbered nodes, where every node has a fixed number (let's say 4) of neighbors. Taking advantage of the above rule, you do it like this:

class Node {
    private:
        // iterators to neighboring nodes
        std::map<int, Node>::iterator neighbors[4];
        friend class Graph;
};

class Graph {
    private:
        std::map<int, Node> nodes;
};

(EDIT: Not literally like this due to the incompleteness of Node in line 4 (see responses/comments), but along these lines anyway)

This is good, because this way you can insert and delete nodes without invalidating the consistency of the structure (assuming you keep track of deletions and remove the deleted iterator from every node's array).

But let's say you also want to be able to store an "invalid" or "nonexistent" neighbor value. Not to worry, we can just use nodes.end()... or can we? Is there some sort of guarantee that nodes.end() at 8 AM will be the same as nodes.end() at 10 PM after a zillion insertions/deletions? That is, can I safely == compare an iterator received as a parameter to nodes.end() in some method of Graph?

And if not, would this work?

class Graph {
    private:
        std::map<int, Node> nodes;
        std::map<int, Node>::iterator _INVALID;
    public:
        Graph() { _INVALID = nodes.end(); }
};

That is, can I store nodes.end() in a variable upon construction, and then use this variable whenever I want to set a neighbor to invalid state, or to compare it against a parameter in a method? Or is it possible that somewhere down the line a valid iterator pointing to an existing object will compare equal to _INVALID?

And if this doesn't work either, what can I do to leave room for an invalid neighbor value?

suszterpatt
  • 8,187
  • 39
  • 60
  • 3
    That's a really good question. – Fragsworth Sep 16 '09 at 10:49
  • 4
    Actually, I just did the obvious thing and **checked 23.1.2/8 in the standard**: "The insert members shall not affect the validity of iterators and references to the container, and the erase members shall invalidate only iterators and references to the erased elements." This is unambiguous: the only possible interpretation is that end() iterators will *not* be invalidated. Thanks to Kirill V. Lyadvinsky for prompting me to check! – j_random_hacker Sep 16 '09 at 12:22
  • 3
    The standard does explicitly state that `end()` will always remain valid, but from what I can tell, all that means is that calling `end()` will not fail. The result of a comparison between two `end()`s queried at different times is, at least to me, an entirely different matter. – suszterpatt Sep 16 '09 at 12:35
  • 1
    @suszterpatt: I think that the term "iterator stays valid" for an end iterator only make sense if it compares equal with the current end iterator. See my answer: http://stackoverflow.com/questions/1432216/1432613#1432613 – sbi Sep 16 '09 at 12:44
  • @sbi: that certainly makes sense, but as much as I appreciate common sense, I'm looking for a more tangible answer here. I suppose the question really does boil down to the definition of "validity" in the case of `end()`. – suszterpatt Sep 16 '09 at 13:01
  • @j_random_hacker: Pavel interprets the same phrase differently than you and I do. And while I don't think his interpretation makes sense, I certainly don't consider the phrase unambiguous. – sbi Sep 16 '09 at 14:02
  • @Kirill: The question is not whether, in the case that this is as clear as `1+1`, it will be true even after a million iterations. _The question is whether this is as clear as `1+1`_. – sbi Sep 16 '09 at 14:03
  • 3
    Saying that it remains valid means you can save a value you obtained at 7AM and still use it at 7PM. If it would mean that its value is not equal to the actual `end`, then it would have become a singular iterator, and the standard says about singular iterator values: "results of most expressions are undefined for singular values" at 24.1/5. – Johannes Schaub - litb Sep 16 '09 at 14:05
  • @suszterpatt: Tried to respond on Kirill's post but it seems it was deleted... "Valid" means that all the required behaviour expected of an iterator (e.g. as listed in Table 74 for Forward Iterators) applies. Specifically this includes the requirements that (I) any way of copying or assigning an iterator a to b satisfies a == b as a postcondition, and (II) a == b is an equivalence relation, meaning this equality relationship is transitive (i.e. if a == b and b == c then a == c). Altogether this means that, yes, end() will not be changed by inserts/deletes! :-) – j_random_hacker Sep 16 '09 at 15:24
  • 1
    I think it is clear: end() returns an iterator to the element one passed the end. Insertion/Deletion do not affect existing iterators so the returned values is always valid (unless you try to delete the element one passed then end (but that would result in undefined behavior anyway). Thus any new iterator generated by end (would be different) but when compared with the original using operator== would return true. Also any intermediate values generated using the assignment operator= have a post condition that they compare equal with operator== and operator== is transative for iterators. – Martin York Sep 16 '09 at 16:50

10 Answers10

6

You write (emphasis by me):

§23.1.2.8 in the standard states that insertion/deletion operations on a set/map will not invalidate any iterators to those objects (except iterators pointing to a deleted element).

Actually, the text of 23.1.2/8 is a bit different (again, emphasis by me):

The insert members shall not affect the validity of iterators and references to the container, and the erase members shall invalidate only iterators and references to the erased elements.

I read this as: If you have a map, and somehow obtain an iterator into this map (again: it doesn't say to an object in the map), this iterator will stay valid despite insertion and removal of elements. Assuming std::map<K,V>::end() obtains an "iterator into the map", it should not be invalidated by insertion/removal.

This, of course, leaves the question whether "not invalidated" means it will always have the same value. My personal assumption is that this is not specified. However, in order for the "not invalidated" phrase to make sense, all results of std::map<K,V>::end() for the same map must always compare equal even in the face of insertions/removal:

my_map_t::iterator old_end = my_map.end();
// wildly change my_map
assert( old_end == my_map.end() ); 

My interpretation is that, if old_end remains "valid" throughout changes to the map (as the standard promisses), then that assertion should pass.

Disclaimer: I am not a native speaker and have a very hard time digesting that dreaded legaleze of the Holy PDF. In fact, in general I avoid it like the plague.

Oh, and my first thought also was: The question is interesting from an academic POV, but why doesn't he simply store keys instead of iterators?

sbi
  • 219,715
  • 46
  • 258
  • 445
  • Yes, the essence of the question is whether that interpretation is correct. – suszterpatt Sep 16 '09 at 12:44
  • "The insert members shall not affect the validity of (iterators) and (references to the container)". I think this is the proper parentheses arrangement--at least it makes sense, while your interpretation does not: there's not such a ting as "iterator to the container". However, there exist "iterators *over* containers". – P Shved Sep 16 '09 at 13:45
  • @Pavel: If "there's no such thing as iterators _to the container"_, why would there be references _to the container_? I'm not convinced, although I agree that the phrase is far from being clear. – sbi Sep 16 '09 at 13:59
  • I'm a little bit lost whether to agree or not agree with you, since you seem to say *"My personal assumption is that this is not specified"* , and then you say *"in order for the "not invalidated" phrase to make sense, all results of std::map::end() for the same map must always compare equal"* . This seems to say you assume it's unspecified whether the value remains the same, but the value must remain the same. Doesn't that mean that it's *not* unspecified? – Johannes Schaub - litb Sep 16 '09 at 14:18
  • @sbi: at least, "reference to container" makes sense: `map &ref = my_map`. Not sure what they meant, maybe that the container doesn't delete itself if there are no elements left... – P Shved Sep 16 '09 at 16:49
  • I think that just means in `K const &k = my_map.begin()->first;`, `k` doesn't invalidate. – Johannes Schaub - litb Sep 16 '09 at 16:54
  • It just sounds like they were referring to the container itself because they packed the reference together with the iterators into one sentence. In other cases, it's more clear (c++0x about unordered containers:) *"The insert members shall not affect the validity of references to container elements, but may invalidate all iterators to the container. The erase members shall invalidate only iterators and references to the erased elements."* – Johannes Schaub - litb Sep 16 '09 at 17:19
  • In fact, you can say "iterator to a container". Consider 23.1.1 sequence requirements: "X denotes a sequence class, a denotes a value of X, ..., p denotes a valid iterator to a". – Johannes Schaub - litb Sep 16 '09 at 17:26
  • @litb: That's just my uncertainty showing. (Did I mention I hate reading the Holy PDF?) – sbi Sep 16 '09 at 19:32
  • @Pavel: Since there's no way a reference to an object can become invalid by having the object's value changed, I don't believe this to be a meaningful interpretation. – sbi Sep 16 '09 at 19:33
  • I think he was talking about this: `struct f { void remove_all_elements() { delete this; } }; int main() { f &rf = *new f; rf.remove_all_elements(); /* reference is now invalid */ }` but i think it refers to references to elements, not a reference to container, so this isn't what it means. – Johannes Schaub - litb Sep 16 '09 at 22:14
  • Yeah, I might have overnitpicked :). lib is right: that should mean references to elements of the container. – P Shved Sep 17 '09 at 05:12
  • @litb: OK, so replace the "object" in my comment with "`std::map`". I didn't count in the pathological case of self-deletion because STL containers don't do this. (In fact, your example isn't just smelly. It stinks badly. I know, it's just an example. But still...) – sbi Sep 17 '09 at 14:54
5

23.1/7 says that end() returns an iterator that

is the past-the-end value for the container.

First, it confirms that what end() returns is the iterator. Second, it says that the iterator doesn't point to a particular element. Since deletion can only invalidate iterators that point somewhere (to the element being deleted), deletions can't invalidate end().

P Shved
  • 96,026
  • 17
  • 121
  • 165
  • True, +1, but I think what the OP is getting at is the exact semantics of `operator==()` on past-the-end iterators. For that you need to look in Table 74, which does make the necessary guarantees. – j_random_hacker Sep 16 '09 at 15:41
3

Well, there's nothing preventing particular collection implementation from having end() depend on the instance of collection and time of day, as long as comparisons and such work. Which means, that, perhaps, end() value may change, but old_end == end() comparison should still yield true. (edit: although after reading the comment from j_random_hacker, I doubt this paragraph itself evaluates to true ;-), not universally — see the discussion below )

I also doubt you can use std::map<int,Node>::iterator in the Node class due to the type being incomplete, yet (not sure, though).

Also, since your nodes are uniquely numbered, you can use int for keying them and reserve some value for invalid.

Michael Krelin - hacker
  • 138,757
  • 24
  • 193
  • 173
  • Well, I don't think this could be true in all cases. Consider a `vector x` with 5 items. It must be true that `x.begin() + 5 == x.end()`; but if you save that past-the-end iterator to `old_end` and then add another item to the vector, `old_end != x.end()`. The only way I can see `vector` preserving `old_end == end()` is if it checks the result of every expression that produces an iterator to see whether it matches the "true" past-the-end position and if so immediately converts it to a "special" value analogous to the NULL pointer. But that would mean a big performance hit methinks. – j_random_hacker Sep 16 '09 at 11:25
  • j_random_hacker, makes sense. Then, perhaps, although it will likely hold true for maps, it's not a good thing to rely upon. – Michael Krelin - hacker Sep 16 '09 at 11:33
  • +1 for noting incompleteness of the `std::map` type -- 17.3.4.6/2 specifically prescribes Undefined Behaviour in this case. – j_random_hacker Sep 16 '09 at 11:35
  • heh, thanks, j_random_hacker, but unfortunately, this subconscious syntax check isn't dealing with the question ;-) – Michael Krelin - hacker Sep 16 '09 at 11:36
  • Yes it seems likely to hold for sets and maps, at least when you're inserting/deleting before the last item in the set/map. – j_random_hacker Sep 16 '09 at 11:37
  • 8
    @j_: insertions into a vector invalidate all its iterators: `old_end == end()` would fail because `old_end` isn't valid anymore. My question pertains to (multi)sets and (multi)maps only, as they're the only containers to which §23.1.2.8 applies. – suszterpatt Sep 16 '09 at 11:37
  • Oh, right, suszterpatt, of course the case of all iterators being invalidated can be neglected. But I'll leave my comment, because if I could mislead myself and j_random_hacker, that may also happen to any other reader :)). – Michael Krelin - hacker Sep 16 '09 at 11:41
  • @suszterpatt: Whoops! For some reason I thought the standard was also ambiguous about `end()` iterators for `vector` and so was addressing them "as if" you had asked about all STL containers in general... But you are quite right, the standard is clear about when `end()` is invalidated for `vector`. @hacker: So my 1st comment didn't challenge your original statement after all -- we are still in the same situation of not knowing any guarantees for the set/map case. :( – j_random_hacker Sep 16 '09 at 11:47
  • j_random_hacker, that's true, but it's a pure luck that I haven't blurted out it in universal way claiming it holds true for all possible collections ;-) I really haven't given it enough thought until you and suszterpatt provoked me to. – Michael Krelin - hacker Sep 16 '09 at 12:03
  • 1
    However... Insertions and deltions from a map do not invaliate its iterators. What's true for vectors, isn't true for maps. – Kieveli Sep 16 '09 at 17:53
2

Iterators in (multi)sets and (multi)maps won't be invalidated in insertions and deletions and thus comparing .end() against previous stored values of .end() will always yield true.

Take as an example GNU libstdc++ implementation where .end() in maps returns the default intialized value of Rb_tree_node

From stl_tree.h:

  _M_initialize()
  {
    this->_M_header._M_color = _S_red;
    this->_M_header._M_parent = 0;
    this->_M_header._M_left = &this->_M_header;
    this->_M_header._M_right = &this->_M_header;
  }
piotr
  • 5,657
  • 1
  • 35
  • 60
1

Assuming that (1) map implemented with red-black tree (2) you use same instance "after a zillion insertions/deletions"- answer "Yes".

Relative implmentation I can tell that all incarnation of stl I ever know use the tree algorithm.

Dewfy
  • 23,277
  • 13
  • 73
  • 121
1

A couple points:

1) end() references an element that is past the end of the container. It doesn't change when inserts or deletes change the container because it's not pointing to an element.

2) I think perhaps your idea of storing an array of 4 iterators in the Node could be changed to make the entire problem make more sense. What you want is to add a new iterator type to the Graph object that is capable of iterating over a single node's neighbours. The implementation of this iterator will need to access the members of the map, which possibly leads you down the path of making the Graph class extend the map collection. With the Graph class being an extended std::map, then the language changes, and you no longer need to store an invalid iterator, but instead simply need to write the algorithm to determine who is the 'next neighbour' in the map.

Kieveli
  • 10,944
  • 6
  • 56
  • 81
1

I think it is clear:

  • end() returns an iterator to the element one past the end.

  • Insertion/Deletion do not affect existing iterators so the returned values are always valid (unless you try to delete the element one past the end (but that would result in undefined behavior anyway)).

  • Thus any new iterator generated by end() (would be different but) when compared with the original using operator== would return true.

  • Also any intermediate values generated using the assignment operator= have a post condition that they compare equal with operator== and operator== is transitive for iterators.

So yes, it is valid to store the iterator returned by end() (but only because of the guarantees with associative containers, therefor it would not be valid for vector etc).

Remember the iterator is not necessarily a pointer. It can potentially be an object where the designer of the container has defined all the operations on the class.

hochl
  • 12,524
  • 10
  • 53
  • 87
Martin York
  • 257,169
  • 86
  • 333
  • 562
0

I believe that this depends entirely on what type of iterator is being used.

In a vector, end() is the one past the end pointer and it will obviously change as elements are inserted and removed.

In another kind of container, the end() iterator might be a special value like NULL or a default constructed element. In this case it doesn't change because it doesn't point at anything. Instead of being a pointer-like thing, end() is just a value to compare against.

I believe that set and map iterators are the second kind, but I don't know of anything that requires them to be implemented in that way.

Zan Lynx
  • 53,022
  • 10
  • 79
  • 131
  • I think you made the same slight mistake as I did -- the OP's question is specifically about map/set iterators, since the standard explicitly says when vector iterators are invalidated. (And it turns out that it says so for map/set iterators too -- at least IMHO.) – j_random_hacker Sep 16 '09 at 14:54
0

C++ Standard states that iterators should stay valid. And it is. Standard clearly states that in 23.1.2/8:

The insert members shall not affect the validity of iterators and references to the container, and the erase members shall invalidate only iterators and references to the erased elements.

And in 21.1/7:

end() returns an iterator which is the past-the-end value for the container.

So iterators old_end and new_end will be valid. That means that we could get --old_end (call it it1) and --new_end (call it it2) and it will be the-end value iterators (from definition of what end() returns), since iterator of an associative container is of the bidirectional iterator category (according to 23.1.2/6) and according to definition of --r operation (Table 75).

Now it1 should be equal it2 since it gives the-end value, which is only one (23.1.2/9). Then from 24.1.3 follows that: The condition that a == b implies ++a == ++b. And ++it1 and ++it2 will give old_end and new_end iterators (from definition of ++r operation Table 74). Now we get that old_end and new_end should be equal.

Kirill V. Lyadvinsky
  • 97,037
  • 24
  • 136
  • 212
  • Some good points, but your reasoning below the 2nd code snippet only proves the case for associative containers holding at least 1 element (`--old_end` produces Undefined Behaviour for an empty container). Martin York's answer shows that the same conclusion can be reached in the general case without invoking `operator--()`. – j_random_hacker Sep 16 '09 at 19:16
0

I had a similar question recently, but I was wondering if calling end() to retrieve an iterator for comparison purposes could possibly have race conditions.

According to the standard, two iterators are considered equivalent if both can be dereferenced and &*a == &*b or if neither can be dereferenced. Finding the bolded statement took a while and is very relevant here.

Because an std::map::iterator cannot be invalidated unless the element it points to has been removed, you're guaranteed that two iterators returned by end, regardless of what the state of the map was when they were obtained, will always compare to each other as true.

Collin Dauphinee
  • 13,664
  • 1
  • 40
  • 71