Search code examples
c++gccc++20sanitizer

Iterator invalidation for associative containers


I know that erasing elements from an associative container inside a for loop invalidates it.

Is it the case when using a range based loop?

#include <iostream>
#include <unordered_map>
#include <set>
#include <algorithm>
#include <ranges>

struct A {
    int value;
    // other members...
};

int main() {
    std::unordered_map<std::string, std::set<A>> myMap;

    // Assuming you have populated the map and the set

    // Object of type A
    A a;

    for (auto& [key, valueSet] : myMap) {
        valueSet.erase(a);

        if (valueSet.empty()) {
            myMap.erase(key);
        }
    }

    return 0;
}

And how can the compiler detects it.

I run -fsanitize=address,undefined but gcc doesn't show any warning.


Solution

  • Is it the case when using a range based loop?

    Generally yes, a range-based for loop is just using iterators under the hood. It effectively expands to:

    {   // for (init-statement; range-declaration : range-expression) { ... }
        /* init-statement */
        auto && __range = /* range-expression */;
        auto __begin = /* begin-expr */;
        auto __end = /* end-expr */;
        for ( ; __begin != __end; ++__begin) {
            /* range-declaration */ = *__begin;
            /* ... */
        }
    }
    

    Any operation which invalidates iterators might also interfere with a range-based for loop as a result.

    std::unordered_map::erase invalidates the iterator to the erased element. In your case, erase(key) is unsafe, because it invalidates the __begin iterator which is used under the hood. ++__begin would then advance an invalidated iterator, which is undefined behavior.

    -fsanitize=address,undefined does catch this type of mistake. (See live example at Compiler Explorer). It is a heap-use-after-free. Are you sure that -fsanitize is even supported on your platform? Maybe it doesn't do anything because you're using MinGW and an old version of GCC.