tags:

views:

482

answers:

9

§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?

+1  A: 

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
+3  A: 

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
Well, I don't think this could be true in all cases. Consider a `vector<int> 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
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
+1 for noting incompleteness of the `std::map<int,Node>` type -- 17.3.4.6/2 specifically prescribes Undefined Behaviour in this case.
j_random_hacker
heh, thanks, j_random_hacker, but unfortunately, this subconscious syntax check isn't dealing with the question ;-)
Michael Krelin - hacker
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
@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
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
@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
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
However... Insertions and deltions from a map do not invaliate its iterators. What's true for vectors, isn't true for maps.
Kieveli
Kievel, yes, haven't you read the whole discussion?
Michael Krelin - hacker
+4  A: 

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
Yes, the essence of the question is whether that interpretation is correct.
suszterpatt
"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".
Pavel Shved
@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
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<K,V>::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
Pavel Shved
I think that just means in `K const `, `k` doesn't invalidate.
Johannes Schaub - litb
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
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
@litb: That's just my uncertainty showing. (Did I mention I hate reading the Holy PDF?)
sbi
@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
I think he was talking about this: `struct f { void remove_all_elements() { delete this; } }; int main() { 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
Yeah, I might have overnitpicked :). lib is right: that should mean references to elements of the container.
Pavel Shved
@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
+3  A: 

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().

Pavel Shved
Hahaha. Beat me on this point. +1
Kieveli
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
+1  A: 

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
+1  A: 

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
A: 

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
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
+1  A: 

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 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.

Martin York
A: 
Kirill V. Lyadvinsky
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