4

Here in this Stack Overflow answer it is listed the iterator invalidation rules for the standard containers in C++11. Particularly, there are for insertion:

  1. [multi]{set,map}: all iterators and references unaffected [23.2.4/9]
  2. unordered_[multi]{set,map}: all iterators invalidated when rehashing occurs, but references unaffected [23.2.5/8]. Rehashing does not occur if the insertion does not cause the container's size to exceed z * B where z is the maximum load factor and B the current number of buckets. [23.2.5/14]

    erasure:

  3. [multi]{set,map} and unordered_[multi]{set,map}: only iterators and references to the erased elements are invalidated

Do these rules mean I can safely do insertion and erasure in one thread, and safely in another thread access, look for (using find) elements as long as these elements are not the ones being inserted and erased in the first thread, and make sure that rehashing is not happening?

If not, what do these rules exactly mean?

Community
  • 1
  • 1
Allanqunzi
  • 3,230
  • 1
  • 26
  • 58

4 Answers4

6

The fact that iterators to elements of the container are not invalidated in no way implies thread safety on the container itself. For example, the size member variable would need to be modified atomically which is a totally separate issue from iterators being invalidated (or not) on insertion/deletion.

tl;dr; No.

These rules simply tell you when an iterator to an element is invalidated by an operation. For example, when a vector resizes, the underlying array is reallocated elsewhere so if you had an iterator (or pointer) to an element, it would no longer be valid after the resize (because it would be pointing to deleted elements of the old array).

Borgleader
  • 15,826
  • 5
  • 46
  • 62
  • How about I just access elements using `at` or `[]`, and not using `size()`? How accessing elements using `at` in another thread is a separate issue from these rules? – Allanqunzi Aug 14 '15 at 19:34
  • These rules have nothing to do with thread safety. They *only* tell you what operations invalidate iterators/pointers/references to elements. Those are orthogonal concepts. – Borgleader Aug 14 '15 at 19:42
  • How about, for example, I use `find` to look for the already existing elements and get the found `iterator` and do something about the element pointed by this `iterator`, is this iterator invalidated if there is insertion or erasure for different elements in another thread? – Allanqunzi Aug 14 '15 at 19:44
  • Regardless of iterator invalidation rules, to my knowledge there are no thread safety guarantees on standard library containers. So this is not guaranteed to work. – Borgleader Aug 14 '15 at 19:53
  • @Borgleader your overall answer was no, but is it a yes if we only discuss std::map? – Amir Rasti May 04 '23 at 19:20
4

There are two kinds of operations on C++ std containers. Reader and Writer operations (these are not the terms the standard uses, but this reads easier). In addition, there are operations on elements in the container.

A const method is a Reader method, as are "lookup" functions that are only non-const because they return a non-const reference to an element (or similar). There is a complete list in the standard, but common sense should work. vector::operator[], begin(), map::find() are all "Readers". map::operator[] is not a "Reader".

You can have any number of threads engaging in Reader operations at the same time no problem.

If you have any thread engaged in a Writer operation, no other access can occur on the container.

You cannot safely have one Reader and one Writer at the same time. If you have any Writers, you must have excluded all other access.

You can safely have 1337 readers at once.

Operations on elements is somewhat similar, except that Writing to an element need only exclude other access to that element. And you are responsible for making your const method play nice with each other. (the std guarantees that the const methods of the container will only call const methods of the elements)

Note that changing sorting order in a std:: associative container is UB without modifying the container.

An iterator that is not invalidated, where you just operate on the element, will (I believe) count as operations on the element. Only synchronization on that element is required.

Note that std::vector<bool> does not follow the above rules.


If you violate the above rules, the C++ std library does not stop you. It just states there is a race condition -- aka, undefined behavior. Anything can happen. In C++ std library, if you don't need something, you don't pay for it: and a single-threaded use of those containers would not need the weight of synchronization. So you don't get it by default.

A std::shared_timed_mutex or std::experimental::shared_mutex can both be useful to guarantee the above holds. unique_lock for Writing, and shared_lock for Reading. Write access to elements has to be shared_locked on the container guarded, and somehow guarded against overlapping access to the same element without deadlock.


Iterator invalidation rules are relatively orthogonal to the thread-safety rules.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • I got a little bit confused, do you mean, for example, `std::map::find()` or `std::unordered_map::find()` are look up operations and by "common sense" can be considered `const` and thread safe even though there is one `non-const` version of `find()` ([here](http://en.cppreference.com/w/cpp/container/map/find)), but this is contradicted to what @HowardHinnant answered here. – Allanqunzi Aug 14 '15 at 19:59
  • @Allanqunzi Yes, `find()` is `const`-esque. I don't see the contradiction. `find` won't conflict with `size() const` or `begin()`, but will conflict with `insert` -- the conflict with `insert` is that `insert` is non-`const`. If any access is non-`const`, then no other access is allowed. If all access is `const`, you are fine. And some access is treated as `const` even though it isn't (lookup operations). – Yakk - Adam Nevraumont Aug 14 '15 at 20:15
  • @Allanqunzi rewritten in terms of Reader/Writer instead of `const`-esque. – Yakk - Adam Nevraumont Aug 14 '15 at 20:21
  • You said "If any access is `non-const`, then no other access is allowed", no other access is allowed by who? You mean programer sholdn't write code which allow `insert` and `find` potentially happen at the same time or it is not allowed by the internal implementation of the C++ standard library? – Allanqunzi Aug 14 '15 at 20:21
  • @Allanqunzi All of the above are rules for the programmer to follow. The standard simply states that if the rules are broken, the result is undefined behavior. Enforcing these rules would result in overhead in cases where the rules won't matter (single threaded use, for example), and when using the C++ `std` library you do not pay for what you do not use. You can wrap your containers in reader/writer locks, and avoid external pointers-to-elements, if you need it. – Yakk - Adam Nevraumont Aug 14 '15 at 20:26
3

Using find implies traversal, at least over a subset of the elements. insert and erase on [multi]{set,map} results in rebalancing the underlying tree, which impacts the links between the nodes. If a rebalance happens at the same time as a find, bad things will happen.

Similarly bad things will happen if you attempt a find during unordered_[multi]{set,map} insert or erase. insert can cause rehashing. And both insert and erase need to link/unlink elements from a list. If a find is searching over a list during a link/unlink, you lose.

[] on [unordered][multi]{set,map} is shorthand for "find and insert if not found". at is shorthand for find. So no, these are not safe to use either.

If you have an existing iterator into a [multi]{set,map}, you can continue to dereference (but not increment/decrement) that iterator while another element is inserted or erased. For unordered_[multi]{set,map}, this is true only if you can guarantee that rehashing won't happen under the insert (it never happens under the erase).

T.C.
  • 133,968
  • 17
  • 288
  • 421
Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • What you said about `find` seems in contradiction of the 1st paragraph of what @Yakk replied here. – Allanqunzi Aug 14 '15 at 20:09
  • 3
    @Allanqunzi: At the risk of speaking for Yakk, I believe both of us disagree with you on that assessment. If a const function is run simultaneously with a non-const function, you have a data race. – Howard Hinnant Aug 14 '15 at 20:20
0

There are other answers here who go into the thread safety issue. So if these rules don't mean thread safety where does that leaves us?

If not, what do these rules exactly mean?

They tell you when you can't use an iterator anymore.

Lets take a (deceptive innocent) example:

auto v = std::vector<int>{....};
//v.reserve(...);

for (auto it = std::begin(v); it != std::end(v); ++it) {
   if (*it == ...)
     std::insert(it, ...);
}

Here you traverse a vector and for each element that tests a condition, you insert something into that position.

Now is this code valid? The iterator invalidation rules tells you that if the vector's capacity is big enough the insertion invalidates only iterator after the insert position. So if you can prove that the reserve (commented line) is big enough, then yes, the code is valid. If not, the code is invalid, as the insert invalidates all the iterators of the vector, which means that it is invalidated, which means that you cannot use it anymore. You'd have to have to reacquire it:

auto idx = std::distance(std::begin(v), it);
std::insert(it, ...);
it = std::begin(v) + idx;
bolov
  • 72,283
  • 15
  • 145
  • 224